mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
Merge pull request #1943 from selfxyz/release/staging-2026-04-08
Release to Staging v2.9.16 - 2026-04-08
This commit is contained in:
1
.github/workflows/mobile-e2e.yml
vendored
1
.github/workflows/mobile-e2e.yml
vendored
@@ -10,6 +10,7 @@ env:
|
||||
GH_CACHE_VERSION: v2 # Global cache version - bumped to invalidate caches
|
||||
GH_GEMS_CACHE_VERSION: v1 # Ruby gems cache version
|
||||
# Performance optimizations
|
||||
YARN_TASK_POOL_CONCURRENCY: 2 # Limit parallel native module builds to prevent OOM on self-hosted runners
|
||||
GRADLE_OPTS: -Dorg.gradle.workers.max=4 -Dorg.gradle.parallel=true -Dorg.gradle.caching=true
|
||||
CI: true
|
||||
# Disable Maestro analytics in CI
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -57,7 +57,10 @@ contracts/broadcast/
|
||||
!packages/rn-sdk-test-app/react-native.config.cjs
|
||||
packages/native-shell-android/.gradle/
|
||||
packages/native-shell-android/build/
|
||||
|
||||
# WebView bundles — built from webview-app, not checked in
|
||||
packages/native-shell-android/src/main/assets/self-wallet/
|
||||
packages/native-shell-ios/Resources/self-sdk-web/
|
||||
packages/self-sdk-swift/Sources/SelfSdkSwift/Resources/self-sdk-web/
|
||||
# Isolated Gradle home for format tasks
|
||||
.gradle-home/
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
BUNDLE_PATH: "vendor/bundle"
|
||||
BUNDLE_FORCE_RUBY_PLATFORM: 1
|
||||
|
||||
@@ -12,7 +12,7 @@ gem "fastlane", "~> 2.232.0"
|
||||
|
||||
group :development do
|
||||
gem "dotenv"
|
||||
gem "nokogiri", "~> 1.18", platform: :ruby
|
||||
gem "nokogiri", "~> 1.18"
|
||||
gem "bundler-audit", "~> 0.9", require: false
|
||||
end
|
||||
|
||||
|
||||
@@ -189,6 +189,8 @@ GEM
|
||||
fastlane-sirp (1.0.0)
|
||||
sysrandom (~> 1.0)
|
||||
ffi (1.17.3)
|
||||
ffi (1.17.3-arm64-darwin)
|
||||
ffi (1.17.3-x86_64-darwin)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
@@ -260,6 +262,10 @@ GEM
|
||||
nokogiri (1.19.1)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.1-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.1-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
optparse (0.8.1)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.3)
|
||||
@@ -318,7 +324,9 @@ GEM
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin
|
||||
ruby
|
||||
x86_64-darwin
|
||||
|
||||
DEPENDENCIES
|
||||
activesupport (>= 6.1.7.5, != 7.1.0)
|
||||
|
||||
@@ -1898,7 +1898,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNDeviceInfo (15.0.1):
|
||||
- RNDeviceInfo (15.0.2):
|
||||
- React-Core
|
||||
- RNFBApp (21.14.0):
|
||||
- Firebase/CoreOnly (= 11.11.0)
|
||||
@@ -1978,7 +1978,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNLocalize (3.6.1):
|
||||
- RNLocalize (3.7.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2667,7 +2667,7 @@ SPEC CHECKSUMS:
|
||||
RNAppleAuthentication: a89c9804592b38ed4ab11f0aee68d05ba12ad432
|
||||
RNCAsyncStorage: 6a8127b6987dc9fbce778669b252b14c8355c7ce
|
||||
RNCClipboard: 9f7b908de4bf4353871fb454c15fc03db4917b88
|
||||
RNDeviceInfo: 36d7f232bfe7c9b5c494cb7793230424ed32c388
|
||||
RNDeviceInfo: 4c852998208b60dc192ae3529e5867817719ad1e
|
||||
RNFBApp: 4105e54d9ca4a1c10893a032268470f670181110
|
||||
RNFBMessaging: 6857871d9dff8f26b0c325fc7d97ba69cb77d213
|
||||
RNFBRemoteConfig: 8d3675f18c052483ce294bb97b857428467fb41e
|
||||
@@ -2675,7 +2675,7 @@ SPEC CHECKSUMS:
|
||||
RNGoogleSignin: 60c3f470558dbff0ae54f2f164ef82a89d3eb561
|
||||
RNInAppBrowser: 6d3eb68d471b9834335c664704719b8be1bfdb20
|
||||
RNKeychain: 35beaa17938f7d8e4990d8a38fad5f8a748fc47c
|
||||
RNLocalize: 67cd0eece3ba20fb5dae7625d77f02e88d3d9573
|
||||
RNLocalize: aa57bee9fcd545b98ce773a8e2404f9a36115b4a
|
||||
RNReactNativeHapticFeedback: eb5395b503c7a8f10de5e6722ef8afd3c61bc4f5
|
||||
RNScreens: b0811b109e1a0b8b579f3348018e177bee374840
|
||||
RNSentry: 98ab9f6a16c9596e36565ccf1a5871323f334766
|
||||
|
||||
70
app/ios/scripts/install-ios-deps-if-needed.sh
Executable file
70
app/ios/scripts/install-ios-deps-if-needed.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
IOS_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
APP_DIR=$(cd -- "$IOS_DIR/.." && pwd)
|
||||
REPO_DIR=$(cd -- "$APP_DIR/.." && pwd)
|
||||
|
||||
STAMP_DIR="$IOS_DIR/.cache"
|
||||
STAMP_FILE="$STAMP_DIR/ios-deps.sha256"
|
||||
|
||||
mkdir -p "$STAMP_DIR"
|
||||
|
||||
compute_fingerprint() {
|
||||
local file
|
||||
local files=(
|
||||
"$IOS_DIR/Podfile"
|
||||
"$IOS_DIR/Podfile.lock"
|
||||
"$APP_DIR/Gemfile"
|
||||
"$APP_DIR/Gemfile.lock"
|
||||
"$APP_DIR/package.json"
|
||||
"$APP_DIR/react-native.config.cjs"
|
||||
"$IOS_DIR/local-pods/DiditSDK/DiditSDK.podspec"
|
||||
"$REPO_DIR/yarn.lock"
|
||||
)
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
shasum -a 256 "$file"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
cd "$IOS_DIR"
|
||||
|
||||
if bundle check >/dev/null 2>&1; then
|
||||
echo "Ruby gems already satisfied; skipping bundle install."
|
||||
else
|
||||
echo "Installing Ruby gems..."
|
||||
bundle install
|
||||
fi
|
||||
|
||||
current_fingerprint="$(compute_fingerprint)"
|
||||
saved_fingerprint="$(cat "$STAMP_FILE" 2>/dev/null || true)"
|
||||
pods_in_sync=false
|
||||
|
||||
if [ -d "$IOS_DIR/Pods" ] &&
|
||||
[ -f "$IOS_DIR/Podfile.lock" ] &&
|
||||
[ -f "$IOS_DIR/Pods/Manifest.lock" ] &&
|
||||
cmp -s "$IOS_DIR/Podfile.lock" "$IOS_DIR/Pods/Manifest.lock"; then
|
||||
pods_in_sync=true
|
||||
fi
|
||||
|
||||
if [ "$pods_in_sync" = true ] && [ "$current_fingerprint" = "$saved_fingerprint" ]; then
|
||||
echo "iOS pods already up to date; skipping pod install."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$pods_in_sync" = true ] && [ -z "$saved_fingerprint" ]; then
|
||||
printf '%s\n' "$current_fingerprint" >"$STAMP_FILE"
|
||||
echo "iOS pods already in sync; saved dependency fingerprint and skipped pod install."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Installing iOS pods..."
|
||||
"$SCRIPT_DIR/pod-install-with-cache-fix.sh"
|
||||
|
||||
compute_fingerprint >"$STAMP_FILE"
|
||||
echo "Saved iOS dependency fingerprint to $STAMP_FILE."
|
||||
@@ -1,22 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Pod install with hermes-engine cache fix for React Native upgrades
|
||||
# This script handles CocoaPods cache mismatches that occur after React Native version upgrades
|
||||
# Pod install with an on-demand Hermes cache fix.
|
||||
# Most installs should reuse CocoaPods caches; only clear them after a failure
|
||||
# that is likely caused by a stale Hermes artifact from a React Native upgrade.
|
||||
|
||||
set -e # Exit on any error
|
||||
set -e
|
||||
|
||||
echo "🧹 Clearing CocoaPods cache to prevent hermes-engine version conflicts..."
|
||||
bundle exec pod cache clean --all > /dev/null 2>&1 || true
|
||||
rm -rf ~/Library/Caches/CocoaPods > /dev/null 2>&1 || true
|
||||
|
||||
echo "📦 Attempting pod install..."
|
||||
if bundle exec pod install; then
|
||||
echo "📦 Attempting pod install with existing CocoaPods caches..."
|
||||
if bundle exec pod install --no-repo-update; then
|
||||
echo "✅ Pods installed successfully"
|
||||
else
|
||||
echo "⚠️ Pod install failed, likely due to hermes-engine cache mismatch after React Native upgrade"
|
||||
echo "🔧 Running targeted fix: bundle exec pod update hermes-engine..."
|
||||
bundle exec pod update hermes-engine --no-repo-update
|
||||
echo "🔄 Retrying pod install..."
|
||||
bundle exec pod install
|
||||
echo "✅ Pods installed successfully after cache fix"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "⚠️ pod install failed; clearing Hermes cache and retrying..."
|
||||
bundle exec pod cache clean hermes-engine --all > /dev/null 2>&1 || true
|
||||
|
||||
echo "🔧 Running targeted fix: bundle exec pod update hermes-engine..."
|
||||
bundle exec pod update hermes-engine --no-repo-update --verbose
|
||||
|
||||
echo "🔄 Retrying pod install..."
|
||||
bundle exec pod install --no-repo-update
|
||||
echo "✅ Pods installed successfully after cache fix"
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
"bump-version:major": "npm version major && yarn sync-versions",
|
||||
"bump-version:minor": "npm version minor && yarn sync-versions",
|
||||
"bump-version:patch": "npm version patch && yarn sync-versions",
|
||||
"clean": "yarn clean:watchman && yarn clean:build && yarn clean:ios && yarn clean:xcode && yarn clean:pod-cache && yarn clean:node && yarn clean:android-deps",
|
||||
"clean": "yarn clean:watchman && yarn clean:build && yarn clean:ios && yarn clean:xcode && yarn clean:pod-cache && yarn clean:android-deps && yarn clean:node",
|
||||
"clean:android-deps": "node scripts/cleanup-private-modules.cjs",
|
||||
"clean:build": "rm -rf ios/build android/app/build android/build",
|
||||
"clean:ios": "rm -rf ios/Pods ios/Podfile.lock Gemfile.lock",
|
||||
"clean:node": "rm -rf ../node_modules app/node_modules",
|
||||
"clean:ios": "rm -rf ios/Pods ios/Podfile.lock",
|
||||
"clean:node": "rm -rf ../node_modules node_modules",
|
||||
"clean:pod-cache": "cd ios && pod cache clean --all && cd ..",
|
||||
"clean:ruby": "rm -rf vendor/bundle",
|
||||
"clean:watchman": "watchman watch-del-all",
|
||||
"clean:xcode": "rm -rf ~/Library/Developer/Xcode/DerivedData",
|
||||
"clean:xcode-env-local": "rm -f ios/.xcode.env.local",
|
||||
@@ -32,7 +33,7 @@
|
||||
"postinstall": "npx patch-package --patch-dir ../patches || true",
|
||||
"install-app": "yarn install-app:setup && yarn clean:xcode-env-local",
|
||||
"install-app:mobile-deploy": "yarn install && yarn build:deps && yarn clean:xcode-env-local",
|
||||
"install-app:setup": "yarn install && yarn build:deps && yarn setup:android-deps && cd ios && bundle install && scripts/pod-install-with-cache-fix.sh && cd ..",
|
||||
"install-app:setup": "yarn install && yarn build:deps && yarn setup:android-deps && cd ios && scripts/install-ios-deps-if-needed.sh && cd ..",
|
||||
"ios": "yarn build:deps && node scripts/run-ios-simulator.cjs",
|
||||
"ios:fastlane-debug": "yarn reinstall && bundle exec fastlane --verbose ios internal_test",
|
||||
"jest:clear": "node ./node_modules/jest/bin/jest.js --clearCache",
|
||||
@@ -85,8 +86,8 @@
|
||||
"react-native-webview": "13.16.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.6",
|
||||
"@didit-protocol/sdk-react-native": "^3.2.7",
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@didit-protocol/sdk-react-native": "^3.2.8",
|
||||
"@ethersproject/shims": "^5.8.0",
|
||||
"@invertase/react-native-apple-authentication": "^2.5.1",
|
||||
"@noble/hashes": "^1.5.0",
|
||||
@@ -99,18 +100,18 @@
|
||||
"@react-native-firebase/app": "^21.14.0",
|
||||
"@react-native-firebase/messaging": "^21.14.0",
|
||||
"@react-native-firebase/remote-config": "^21.14.0",
|
||||
"@react-native-google-signin/google-signin": "^16.1.1",
|
||||
"@react-native-google-signin/google-signin": "^16.1.2",
|
||||
"@react-navigation/native": "^7.0.14",
|
||||
"@react-navigation/native-stack": "^7.2.0",
|
||||
"@robinbobin/react-native-google-drive-api-wrapper": "^2.2.3",
|
||||
"@segment/analytics-react-native": "^2.21.2",
|
||||
"@robinbobin/react-native-google-drive-api-wrapper": "^2.2.6",
|
||||
"@segment/analytics-react-native": "^2.22.0",
|
||||
"@segment/sovran-react-native": "^1.1.3",
|
||||
"@selfxyz/common": "workspace:^",
|
||||
"@selfxyz/euclid": "^0.6.1",
|
||||
"@selfxyz/mobile-sdk-alpha": "workspace:^",
|
||||
"@sentry/react": "^9.32.0",
|
||||
"@sentry/react-native": "7.0.0",
|
||||
"@stablelib/cbor": "^2.0.1",
|
||||
"@stablelib/cbor": "^2.0.4",
|
||||
"@tamagui/animations-css": "1.126.14",
|
||||
"@tamagui/animations-react-native": "1.126.14",
|
||||
"@tamagui/config": "1.126.14",
|
||||
@@ -123,11 +124,11 @@
|
||||
"@walletconnect/react-native-compat": "^2.23.0",
|
||||
"@xstate/react": "^5.0.3",
|
||||
"asn1js": "^3.0.7",
|
||||
"axios": "^1.13.2",
|
||||
"axios": "^1.14.0",
|
||||
"buffer": "^6.0.3",
|
||||
"country-emoji": "^1.5.6",
|
||||
"elliptic": "^6.6.1",
|
||||
"ethers": "^6.11.0",
|
||||
"ethers": "^6.16.0",
|
||||
"expo": "~52.0.40",
|
||||
"expo-application": "~6.0.2",
|
||||
"hash.js": "^1.1.7",
|
||||
@@ -136,40 +137,40 @@
|
||||
"js-sha512": "^0.9.0",
|
||||
"lottie-react": "^2.4.1",
|
||||
"lottie-react-native": "7.2.2",
|
||||
"node-forge": "^1.3.3",
|
||||
"pkijs": "^3.3.3",
|
||||
"node-forge": "^1.4.0",
|
||||
"pkijs": "^3.4.0",
|
||||
"poseidon-lite": "^0.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-native": "0.77.0",
|
||||
"react-native-app-auth": "^8.0.3",
|
||||
"react-native-app-auth": "^8.1.0",
|
||||
"react-native-biometrics": "^3.0.1",
|
||||
"react-native-blur-effect": "^1.1.3",
|
||||
"react-native-check-version": "^1.3.0",
|
||||
"react-native-check-version": "^1.4.0",
|
||||
"react-native-cloud-storage": "^2.2.2",
|
||||
"react-native-device-info": "^15.0.1",
|
||||
"react-native-device-info": "^15.0.2",
|
||||
"react-native-dotenv": "^3.4.11",
|
||||
"react-native-edge-to-edge": "^1.7.0",
|
||||
"react-native-edge-to-edge": "^1.8.1",
|
||||
"react-native-gesture-handler": "~2.22.0",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
"react-native-haptic-feedback": "^2.3.3",
|
||||
"react-native-inappbrowser-reborn": "^3.7.0",
|
||||
"react-native-inappbrowser-reborn": "^3.7.1",
|
||||
"react-native-keychain": "^10.0.0",
|
||||
"react-native-linear-gradient": "^2.8.3",
|
||||
"react-native-localize": "^3.6.1",
|
||||
"react-native-logs": "^5.5.0",
|
||||
"react-native-localize": "^3.7.0",
|
||||
"react-native-logs": "^5.6.0",
|
||||
"react-native-nfc-manager": "3.17.2",
|
||||
"react-native-passkey": "^3.3.2",
|
||||
"react-native-passkey": "^3.3.3",
|
||||
"react-native-passport-reader": "1.0.3",
|
||||
"react-native-safe-area-context": "^5.6.2",
|
||||
"react-native-safe-area-context": "^5.7.0",
|
||||
"react-native-screens": "4.15.3",
|
||||
"react-native-sqlite-storage": "^6.0.1",
|
||||
"react-native-svg": "15.14.0",
|
||||
"react-native-svg-web": "1.0.9",
|
||||
"react-native-url-polyfill": "^3.0.0",
|
||||
"react-native-web": "^0.21.2",
|
||||
"react-native-webview": "^13.16.0",
|
||||
"react-qr-barcode-scanner": "^2.1.8",
|
||||
"react-native-webview": "13.16.0",
|
||||
"react-qr-barcode-scanner": "^2.1.25",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"tamagui": "1.126.14",
|
||||
"uuid": "^11.1.0",
|
||||
@@ -177,13 +178,13 @@
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.6",
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/plugin-syntax-flow": "^7.28.6",
|
||||
"@babel/plugin-transform-classes": "^7.28.6",
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.27.1",
|
||||
"@babel/plugin-transform-private-methods": "^7.28.6",
|
||||
"@babel/preset-env": "^7.28.6",
|
||||
"@babel/preset-env": "^7.29.2",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@react-native-community/cli": "^16.0.3",
|
||||
"@react-native/babel-preset": "0.77.0",
|
||||
@@ -194,7 +195,7 @@
|
||||
"@tamagui/types": "1.126.14",
|
||||
"@tamagui/vite-plugin": "1.126.14",
|
||||
"@testing-library/react-native": "^13.3.3",
|
||||
"@tsconfig/react-native": "^3.0.6",
|
||||
"@tsconfig/react-native": "^3.0.9",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/elliptic": "^6.4.18",
|
||||
"@types/jest": "^30.0.0",
|
||||
@@ -202,31 +203,31 @@
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-native-dotenv": "^0.2.0",
|
||||
"@types/react-native-dotenv": "^0.2.2",
|
||||
"@types/react-native-sqlite-storage": "^6.0.5",
|
||||
"@types/react-native-web": "^0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.39.0",
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"@vitejs/plugin-react-swc": "^4.3.0",
|
||||
"babel-plugin-module-resolver": "^5.0.3",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"constants-browserify": "^1.0.0",
|
||||
"dompurify": "^3.3.1",
|
||||
"dompurify": "^3.3.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
"eslint-plugin-ft-flow": "^3.0.11",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jest": "^29.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jest": "^29.15.1",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-sort-exports": "^0.9.1",
|
||||
"hermes-eslint": "^0.33.3",
|
||||
"jest": "^30.2.0",
|
||||
"prettier": "^3.5.3",
|
||||
"jest": "^30.3.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-native-svg-transformer": "^1.5.2",
|
||||
"react-native-svg-transformer": "^1.5.3",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
"stream-browserify": "^3.0.0",
|
||||
|
||||
@@ -33,6 +33,7 @@ const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
||||
const repoToken = process.env.SELFXYZ_INTERNAL_REPO_PAT;
|
||||
const appToken = process.env.SELFXYZ_APP_TOKEN; // GitHub App installation token
|
||||
const isDryRun = process.env.DRY_RUN === 'true';
|
||||
const forceAndroidDeps = process.env.FORCE_ANDROID_DEPS === '1';
|
||||
|
||||
// Platform detection for Android-specific modules
|
||||
function shouldSetupAndroidModule() {
|
||||
@@ -240,10 +241,114 @@ function validateSetup(modulePath, validationFiles, repoName) {
|
||||
log(`${repoName} validation passed`, 'success');
|
||||
}
|
||||
|
||||
function isExistingModuleReusable(module) {
|
||||
const { repoName, localPath, validationFiles } = module;
|
||||
|
||||
if (!fs.existsSync(localPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.join(localPath, '.git'))) {
|
||||
log(
|
||||
`Existing ${repoName} checkout is missing .git metadata; recloning`,
|
||||
'warning',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
validateSetup(localPath, validationFiles, repoName);
|
||||
} catch (error) {
|
||||
log(
|
||||
`Existing ${repoName} checkout is invalid: ${error.message}`,
|
||||
'warning',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const remoteUrl = execSync('git remote get-url origin', {
|
||||
cwd: localPath,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
|
||||
const canonicalOriginPattern = new RegExp(
|
||||
`^(git@github\\.com:|https://github\\.com/|ssh://git@github\\.com/)${GITHUB_ORG}/${repoName}(\\.git)?$`,
|
||||
);
|
||||
if (!canonicalOriginPattern.test(remoteUrl)) {
|
||||
const safeUrl = remoteUrl.replace(/\/\/[^@]+@/, '//***@');
|
||||
log(
|
||||
`Existing ${repoName} checkout points at unexpected origin ${safeUrl}; recloning`,
|
||||
'warning',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
log(
|
||||
`Could not inspect origin for existing ${repoName} checkout: ${error.message}`,
|
||||
'warning',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject dirty checkouts — uncommitted changes compromise integrity
|
||||
try {
|
||||
const status = execSync('git status --porcelain', {
|
||||
cwd: localPath,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
|
||||
if (status.length > 0) {
|
||||
log(
|
||||
`Existing ${repoName} checkout has uncommitted changes; recloning`,
|
||||
'warning',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
log(
|
||||
`Could not check dirty state for ${repoName}: ${error.message}`,
|
||||
'warning',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// When a specific commit is pinned, verify HEAD matches
|
||||
if (module.commit) {
|
||||
try {
|
||||
const head = execSync('git rev-parse HEAD', {
|
||||
cwd: localPath,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
|
||||
if (head !== module.commit) {
|
||||
log(
|
||||
`Existing ${repoName} is at ${head.slice(0, 12)} but expected ${module.commit.slice(0, 12)}; recloning`,
|
||||
'warning',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
log(`Could not read HEAD for ${repoName}: ${error.message}`, 'warning');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function setupPrivateModule(module) {
|
||||
const { repoName, localPath, validationFiles, commit } = module;
|
||||
log(`Starting setup of ${repoName}...`, 'info');
|
||||
|
||||
if (!forceAndroidDeps && isExistingModuleReusable(module)) {
|
||||
log(`${repoName} already present; reusing existing checkout`, 'success');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove existing module
|
||||
removeExistingModule(localPath, repoName);
|
||||
|
||||
@@ -344,5 +449,6 @@ if (require.main === module) {
|
||||
module.exports = {
|
||||
setupAndroidPassportReader,
|
||||
removeExistingModule,
|
||||
isExistingModuleReusable,
|
||||
PRIVATE_MODULES,
|
||||
};
|
||||
|
||||
1
contracts/.gitignore
vendored
1
contracts/.gitignore
vendored
@@ -8,6 +8,7 @@ typechain-types
|
||||
#Hardhat files
|
||||
cache
|
||||
artifacts
|
||||
edr-cache
|
||||
ignition/deployed_addresses.json
|
||||
ignition/parameters.json
|
||||
|
||||
|
||||
0
contracts/contracts/registry/IdentityRegistry.sol
Normal file → Executable file
0
contracts/contracts/registry/IdentityRegistry.sol
Normal file → Executable file
222
contracts/contracts/registry/IdentityRegistryAadhaarImplV1.sol
Normal file → Executable file
222
contracts/contracts/registry/IdentityRegistryAadhaarImplV1.sol
Normal file → Executable file
@@ -6,6 +6,29 @@ import {InternalLeanIMT, LeanIMTData} from "@zk-kit/imt.sol/internal/InternalLea
|
||||
import {IIdentityRegistryAadhaarV1} from "../interfaces/IIdentityRegistryAadhaarV1.sol";
|
||||
import {ImplRoot} from "../upgradeable/ImplRoot.sol";
|
||||
import {AttestationId} from "../constants/AttestationId.sol";
|
||||
import {GCPJWTHelper} from "../libraries/GCPJWTHelper.sol";
|
||||
import {Formatter} from "../libraries/Formatter.sol";
|
||||
|
||||
/**
|
||||
* @title IGCPJWTVerifier
|
||||
* @notice Interface for the GCP JWT verifier contract.
|
||||
*/
|
||||
interface IGCPJWTVerifier {
|
||||
function verifyProof(
|
||||
uint256[2] calldata pA,
|
||||
uint256[2][2] calldata pB,
|
||||
uint256[2] calldata pC,
|
||||
uint256[20] calldata pubSignals
|
||||
) external view returns (bool);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title IPCR0Manager
|
||||
* @notice Interface for the PCR0 (TEE image hash) manager contract.
|
||||
*/
|
||||
interface IPCR0Manager {
|
||||
function isPCR0Set(bytes calldata pcr0) external view returns (bool);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice ⚠️ CRITICAL STORAGE LAYOUT WARNING ⚠️
|
||||
@@ -64,6 +87,24 @@ abstract contract IdentityRegistryAadhaarStorageV1 is ImplRoot {
|
||||
|
||||
/// @notice Current name and year of birth OFAC root.
|
||||
uint256 internal _nameAndYobOfacRoot;
|
||||
|
||||
/// @notice Previous name and date of birth OFAC root (rolling window).
|
||||
uint256 internal _prevNameAndDobOfacRoot;
|
||||
|
||||
/// @notice Previous name and year of birth OFAC root (rolling window).
|
||||
uint256 internal _prevNameAndYobOfacRoot;
|
||||
|
||||
/// @notice Address of the GCP JWT verifier contract for OFAC proof updates.
|
||||
address internal _gcpJwtVerifier;
|
||||
|
||||
/// @notice Address of the PCR0Manager for OFAC proof updates.
|
||||
address internal _pcr0Manager;
|
||||
|
||||
/// @notice Expected hash of the GCP root CA public key for OFAC proof verification.
|
||||
uint256 internal _gcpRootCAPubkeyHash;
|
||||
|
||||
/// @notice Address of the TEE authorized to call updateOfacRootsWithProof.
|
||||
address internal _tee;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +112,7 @@ abstract contract IdentityRegistryAadhaarStorageV1 is ImplRoot {
|
||||
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
|
||||
* @dev Inherits from IdentityRegistryAadhaarStorageV1 and implements IIdentityRegistryAadhaarV1.
|
||||
*
|
||||
* @custom:version 1.2.0
|
||||
* @custom:version 1.3.1
|
||||
*/
|
||||
contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIdentityRegistryAadhaarV1 {
|
||||
using InternalLeanIMT for LeanIMTData;
|
||||
@@ -123,6 +164,16 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
event DevCommitmentUpdated(uint256 indexed oldLeaf, uint256 indexed newLeaf, uint256 imtRoot, uint256 timestamp);
|
||||
/// @notice Emitted when a identity commitment is removed by dev team.
|
||||
event DevCommitmentRemoved(uint256 indexed oldLeaf, uint256 imtRoot, uint256 timestamp);
|
||||
/// @notice Emitted when OFAC roots are updated via proof.
|
||||
event OfacRootsUpdatedWithProof(bytes32 rootsHash, uint256 timestamp);
|
||||
/// @notice Emitted when the GCP JWT verifier address is updated.
|
||||
event GCPJWTVerifierUpdated(address gcpJwtVerifier);
|
||||
/// @notice Emitted when the PCR0Manager address is updated.
|
||||
event PCR0ManagerUpdated(address pcr0Manager);
|
||||
/// @notice Emitted when the GCP root CA pubkey hash is updated.
|
||||
event GCPRootCAPubkeyHashUpdated(uint256 gcpRootCAPubkeyHash);
|
||||
/// @notice Emitted when the TEE address is updated.
|
||||
event TEEUpdated(address tee);
|
||||
|
||||
// ====================================================
|
||||
// Errors
|
||||
@@ -136,6 +187,22 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
error REGISTERED_COMMITMENT();
|
||||
/// @notice Thrown when the hub address is set to the zero address.
|
||||
error HUB_ADDRESS_ZERO();
|
||||
/// @notice Thrown when the GCP JWT proof verification fails.
|
||||
error INVALID_PROOF();
|
||||
/// @notice Thrown when the GCP root CA public key hash does not match the expected value.
|
||||
error INVALID_ROOT_CA();
|
||||
/// @notice Thrown when the TEE image hash is not registered in the PCR0Manager.
|
||||
error INVALID_IMAGE();
|
||||
/// @notice Thrown when the timestamp is invalid.
|
||||
error INVALID_TIMESTAMP();
|
||||
/// @notice Thrown when the roots hash does not match the proof.
|
||||
error InvalidRootsHash();
|
||||
/// @notice Thrown when the wrong number of roots is provided.
|
||||
error InvalidRootsCount();
|
||||
/// @notice Thrown when the TEE address is not set.
|
||||
error TEE_NOT_SET();
|
||||
/// @notice Thrown when a function is accessed by an address other than the designated TEE.
|
||||
error ONLY_TEE_CAN_ACCESS();
|
||||
|
||||
// ====================================================
|
||||
// Modifiers
|
||||
@@ -148,6 +215,16 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Modifier to restrict access to functions to only the TEE.
|
||||
* @dev Reverts if the TEE is not set or if the caller is not the TEE.
|
||||
*/
|
||||
modifier onlyTEE() {
|
||||
if (address(_tee) == address(0)) revert TEE_NOT_SET();
|
||||
if (msg.sender != address(_tee)) revert ONLY_TEE_CAN_ACCESS();
|
||||
_;
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// Constructor
|
||||
// ====================================================
|
||||
@@ -184,6 +261,25 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
__ImplRoot_init();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes OFAC proof verification infrastructure.
|
||||
* @dev Sets GCP JWT verifier, PCR0Manager, and root CA pubkey hash.
|
||||
* @param gcpJwtVerifier_ The GCP JWT Groth16 verifier address.
|
||||
* @param pcr0Manager_ The PCR0Manager address for TEE image validation.
|
||||
* @param gcpRootCAPubkeyHash_ The expected Poseidon hash of the GCP root CA public key.
|
||||
*/
|
||||
function initializeOfacProof(
|
||||
address gcpJwtVerifier_,
|
||||
address pcr0Manager_,
|
||||
uint256 gcpRootCAPubkeyHash_,
|
||||
address teeAddress_
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) reinitializer(3) {
|
||||
_gcpJwtVerifier = gcpJwtVerifier_;
|
||||
_pcr0Manager = pcr0Manager_;
|
||||
_gcpRootCAPubkeyHash = gcpRootCAPubkeyHash_;
|
||||
_tee = teeAddress_;
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// External Functions - View & Checks
|
||||
// ====================================================
|
||||
@@ -253,6 +349,32 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
return _nameAndYobOfacRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Retrieves the previous name and date of birth OFAC root (rolling window).
|
||||
* @return The stored previous name and date of birth OFAC root.
|
||||
*/
|
||||
function getPrevNameAndDobOfacRoot() external view onlyProxy returns (uint256) {
|
||||
return _prevNameAndDobOfacRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Retrieves the previous name and year of birth OFAC root (rolling window).
|
||||
* @return The stored previous name and year of birth OFAC root.
|
||||
*/
|
||||
function getPrevNameAndYobOfacRoot() external view onlyProxy returns (uint256) {
|
||||
return _prevNameAndYobOfacRoot;
|
||||
}
|
||||
|
||||
/// @notice Returns the address of the GCP JWT verifier contract.
|
||||
function getGcpJwtVerifier() external view onlyProxy returns (address) {
|
||||
return _gcpJwtVerifier;
|
||||
}
|
||||
|
||||
/// @notice Returns the address of the PCR0 Manager contract.
|
||||
function getPcr0Manager() external view onlyProxy returns (address) {
|
||||
return _pcr0Manager;
|
||||
}
|
||||
|
||||
/// @notice Validates whether the provided OFAC roots match the stored values.
|
||||
/// @param nameAndDobRoot The name and date of birth OFAC root to validate.
|
||||
/// @param nameAndYobRoot The name and year of birth OFAC root to validate.
|
||||
@@ -261,7 +383,11 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
uint256 nameAndDobRoot,
|
||||
uint256 nameAndYobRoot
|
||||
) external view virtual onlyProxy returns (bool) {
|
||||
return _nameAndDobOfacRoot == nameAndDobRoot && _nameAndYobOfacRoot == nameAndYobRoot;
|
||||
bool currentMatch = (_nameAndDobOfacRoot == nameAndDobRoot) && (_nameAndYobOfacRoot == nameAndYobRoot);
|
||||
bool prevMatch = (_prevNameAndDobOfacRoot != 0) &&
|
||||
(_prevNameAndDobOfacRoot == nameAndDobRoot) &&
|
||||
(_prevNameAndYobOfacRoot == nameAndYobRoot);
|
||||
return currentMatch || prevMatch;
|
||||
}
|
||||
|
||||
/// @notice Checks if the provided UIDAI pubkey is stored in the registry and also if it's not expired.
|
||||
@@ -306,6 +432,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||
/// @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
|
||||
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_prevNameAndDobOfacRoot = _nameAndDobOfacRoot;
|
||||
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
|
||||
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
|
||||
}
|
||||
@@ -314,6 +441,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||
/// @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
|
||||
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_prevNameAndYobOfacRoot = _nameAndYobOfacRoot;
|
||||
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
|
||||
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
|
||||
}
|
||||
@@ -342,6 +470,96 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
emit UidaiPubkeyCommitmentUpdated(commitment, block.timestamp);
|
||||
}
|
||||
|
||||
/// @notice Updates the GCP JWT verifier contract address.
|
||||
/// @param verifier The new GCP JWT verifier address.
|
||||
function updateGCPJWTVerifier(address verifier) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_gcpJwtVerifier = verifier;
|
||||
emit GCPJWTVerifierUpdated(verifier);
|
||||
}
|
||||
|
||||
/// @notice Updates the PCR0Manager address.
|
||||
/// @param newPCR0Manager The new PCR0Manager address.
|
||||
function updatePCR0Manager(address newPCR0Manager) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_pcr0Manager = newPCR0Manager;
|
||||
emit PCR0ManagerUpdated(newPCR0Manager);
|
||||
}
|
||||
|
||||
/// @notice Updates the GCP root CA pubkey hash.
|
||||
/// @param newHash The new GCP root CA pubkey hash value.
|
||||
function updateGCPRootCAPubkeyHash(uint256 newHash) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_gcpRootCAPubkeyHash = newHash;
|
||||
emit GCPRootCAPubkeyHashUpdated(newHash);
|
||||
}
|
||||
|
||||
/// @notice Updates the TEE address.
|
||||
/// @param teeAddress The new TEE address.
|
||||
function updateTEE(address teeAddress) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_tee = teeAddress;
|
||||
emit TEEUpdated(teeAddress);
|
||||
}
|
||||
|
||||
/// @notice Retrieves the TEE address.
|
||||
/// @return The current TEE address.
|
||||
function tee() external view onlyProxy returns (address) {
|
||||
return _tee;
|
||||
}
|
||||
|
||||
/// @notice Updates OFAC roots via proof-verified TEE attestation.
|
||||
/// @dev Verifies the Groth16 proof, validates TEE attestation claims, checks
|
||||
/// this registry's roots hash against the eat_nonce from the proof. Restricted to the TEE address. The proof provides
|
||||
/// cryptographic verification, and onlyTEE provides access control.
|
||||
/// @param pA Groth16 proof element A.
|
||||
/// @param pB Groth16 proof element B.
|
||||
/// @param pC Groth16 proof element C.
|
||||
/// @param pubSignals Circuit public signals [rootCA, eatNonce[0-2], unused, imageHash[0-2], date[0-11]].
|
||||
/// @param roots This registry's roots: [nameAndDob, nameAndYob].
|
||||
function updateOfacRootsWithProof(
|
||||
uint256[2] calldata pA,
|
||||
uint256[2][2] calldata pB,
|
||||
uint256[2] calldata pC,
|
||||
uint256[20] calldata pubSignals,
|
||||
uint256[] calldata roots
|
||||
) external onlyProxy onlyTEE {
|
||||
if (roots.length != 2) revert InvalidRootsCount();
|
||||
|
||||
// Verify Groth16 proof
|
||||
if (!IGCPJWTVerifier(_gcpJwtVerifier).verifyProof(pA, pB, pC, pubSignals)) revert INVALID_PROOF();
|
||||
|
||||
// Verify root CA pubkey hash
|
||||
if (pubSignals[0] != _gcpRootCAPubkeyHash) revert INVALID_ROOT_CA();
|
||||
|
||||
// Verify TEE image hash
|
||||
bytes memory imageHash = GCPJWTHelper.unpackAndConvertImageHash(pubSignals[5], pubSignals[6], pubSignals[7]);
|
||||
if (!IPCR0Manager(_pcr0Manager).isPCR0Set(imageHash)) revert INVALID_IMAGE();
|
||||
|
||||
// Verify timestamp (±1 hour)
|
||||
uint256 currentTimestamp = Formatter.toTimeStampWithSeconds(
|
||||
2000 + pubSignals[8] * 10 + pubSignals[9],
|
||||
pubSignals[10] * 10 + pubSignals[11],
|
||||
pubSignals[12] * 10 + pubSignals[13],
|
||||
pubSignals[14] * 10 + pubSignals[15],
|
||||
pubSignals[16] * 10 + pubSignals[17],
|
||||
pubSignals[18] * 10 + pubSignals[19]
|
||||
);
|
||||
if (currentTimestamp + 1 hours < block.timestamp) revert INVALID_TIMESTAMP();
|
||||
if (currentTimestamp > block.timestamp + 1 hours) revert INVALID_TIMESTAMP();
|
||||
|
||||
// Verify roots hash matches eat_nonce from proof
|
||||
bytes32 myHash = sha256(abi.encodePacked(roots[0], roots[1]));
|
||||
uint256 rootsHashFromProof = GCPJWTHelper.unpackAndDecodeHexPubkey(pubSignals[1], pubSignals[2], pubSignals[3]);
|
||||
if (uint256(myHash) != rootsHashFromProof) revert InvalidRootsHash();
|
||||
|
||||
// Update this registry's roots: [nameAndDob, nameAndYob] (with rolling window)
|
||||
_prevNameAndDobOfacRoot = _nameAndDobOfacRoot;
|
||||
_nameAndDobOfacRoot = roots[0];
|
||||
_prevNameAndYobOfacRoot = _nameAndYobOfacRoot;
|
||||
_nameAndYobOfacRoot = roots[1];
|
||||
|
||||
emit NameAndDobOfacRootUpdated(roots[0]);
|
||||
emit NameAndYobOfacRootUpdated(roots[1]);
|
||||
emit OfacRootsUpdatedWithProof(myHash, block.timestamp);
|
||||
}
|
||||
|
||||
/// @notice (DEV) Force-adds an identity commitment.
|
||||
/// @dev Callable only by the owner for testing or administration.
|
||||
/// @param attestationId The identifier for the attestation.
|
||||
|
||||
228
contracts/contracts/registry/IdentityRegistryIdCardImplV1.sol
Normal file → Executable file
228
contracts/contracts/registry/IdentityRegistryIdCardImplV1.sol
Normal file → Executable file
@@ -5,6 +5,29 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U
|
||||
import {InternalLeanIMT, LeanIMTData} from "@zk-kit/imt.sol/internal/InternalLeanIMT.sol";
|
||||
import {IIdentityRegistryIdCardV1} from "../interfaces/IIdentityRegistryIdCardV1.sol";
|
||||
import {ImplRoot} from "../upgradeable/ImplRoot.sol";
|
||||
import {GCPJWTHelper} from "../libraries/GCPJWTHelper.sol";
|
||||
import {Formatter} from "../libraries/Formatter.sol";
|
||||
|
||||
/**
|
||||
* @title IGCPJWTVerifier
|
||||
* @notice Interface for the GCP JWT verifier contract.
|
||||
*/
|
||||
interface IGCPJWTVerifier {
|
||||
function verifyProof(
|
||||
uint256[2] calldata pA,
|
||||
uint256[2][2] calldata pB,
|
||||
uint256[2] calldata pC,
|
||||
uint256[20] calldata pubSignals
|
||||
) external view returns (bool);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title IPCR0Manager
|
||||
* @notice Interface for the PCR0 (TEE image hash) manager contract.
|
||||
*/
|
||||
interface IPCR0Manager {
|
||||
function isPCR0Set(bytes calldata pcr0) external view returns (bool);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice ⚠️ CRITICAL STORAGE LAYOUT WARNING ⚠️
|
||||
@@ -35,8 +58,8 @@ import {ImplRoot} from "../upgradeable/ImplRoot.sol";
|
||||
*/
|
||||
|
||||
/**
|
||||
* @title IdentityRegistryStorageV1
|
||||
* @dev Abstract contract for storage layout of IdentityRegistryImplV1.
|
||||
* @title IdentityRegistryIdCardStorageV1
|
||||
* @dev Abstract contract for storage layout of IdentityRegistryIdCardImplV1.
|
||||
* Inherits from ImplRoot to provide upgradeable functionality.
|
||||
*/
|
||||
abstract contract IdentityRegistryIdCardStorageV1 is ImplRoot {
|
||||
@@ -71,14 +94,32 @@ abstract contract IdentityRegistryIdCardStorageV1 is ImplRoot {
|
||||
|
||||
/// @notice Current CSCA root.
|
||||
uint256 internal _cscaRoot;
|
||||
|
||||
/// @notice Previous name and date of birth OFAC root (rolling window).
|
||||
uint256 internal _prevNameAndDobOfacRoot;
|
||||
|
||||
/// @notice Previous name and year of birth OFAC root (rolling window).
|
||||
uint256 internal _prevNameAndYobOfacRoot;
|
||||
|
||||
/// @notice Address of the GCP JWT verifier contract for OFAC proof updates.
|
||||
address internal _gcpJwtVerifier;
|
||||
|
||||
/// @notice Address of the PCR0Manager for OFAC proof updates.
|
||||
address internal _pcr0Manager;
|
||||
|
||||
/// @notice Expected hash of the GCP root CA public key for OFAC proof verification.
|
||||
uint256 internal _gcpRootCAPubkeyHash;
|
||||
|
||||
/// @notice Address of the TEE authorized to call updateOfacRootsWithProof.
|
||||
address internal _tee;
|
||||
}
|
||||
|
||||
/**
|
||||
* @title IdentityRegistryImplV1
|
||||
* @title IdentityRegistryIdCardImplV1
|
||||
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
|
||||
* @dev Inherits from IdentityRegistryStorageV1 and implements IIdentityRegistryV1.
|
||||
*
|
||||
* @custom:version 1.2.0
|
||||
* @custom:version 1.3.1
|
||||
*/
|
||||
contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdentityRegistryIdCardV1 {
|
||||
using InternalLeanIMT for LeanIMTData;
|
||||
@@ -131,6 +172,16 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
event DevNullifierStateChanged(bytes32 indexed attestationId, uint256 indexed nullifier, bool state);
|
||||
/// @notice Emitted when the state of a DSC key commitment is changed by dev team.
|
||||
event DevDscKeyCommitmentStateChanged(uint256 indexed commitment, bool state);
|
||||
/// @notice Emitted when OFAC roots are updated via proof.
|
||||
event OfacRootsUpdatedWithProof(bytes32 rootsHash, uint256 timestamp);
|
||||
/// @notice Emitted when the GCP JWT verifier address is updated.
|
||||
event GCPJWTVerifierUpdated(address gcpJwtVerifier);
|
||||
/// @notice Emitted when the PCR0Manager address is updated.
|
||||
event PCR0ManagerUpdated(address pcr0Manager);
|
||||
/// @notice Emitted when the GCP root CA pubkey hash is updated.
|
||||
event GCPRootCAPubkeyHashUpdated(uint256 gcpRootCAPubkeyHash);
|
||||
/// @notice Emitted when the TEE address is updated.
|
||||
event TEEUpdated(address tee);
|
||||
|
||||
// ====================================================
|
||||
// Errors
|
||||
@@ -142,6 +193,22 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
error ONLY_HUB_CAN_ACCESS();
|
||||
/// @notice Thrown when attempting to register a commitment that has already been registered.
|
||||
error REGISTERED_COMMITMENT();
|
||||
/// @notice Thrown when the GCP JWT proof verification fails.
|
||||
error INVALID_PROOF();
|
||||
/// @notice Thrown when the GCP root CA public key hash does not match the expected value.
|
||||
error INVALID_ROOT_CA();
|
||||
/// @notice Thrown when the TEE image hash is not registered in the PCR0Manager.
|
||||
error INVALID_IMAGE();
|
||||
/// @notice Thrown when the timestamp is invalid.
|
||||
error INVALID_TIMESTAMP();
|
||||
/// @notice Thrown when the roots hash does not match the proof.
|
||||
error InvalidRootsHash();
|
||||
/// @notice Thrown when the wrong number of roots is provided.
|
||||
error InvalidRootsCount();
|
||||
/// @notice Thrown when the TEE address is not set.
|
||||
error TEE_NOT_SET();
|
||||
/// @notice Thrown when a function is accessed by an address other than the designated TEE.
|
||||
error ONLY_TEE_CAN_ACCESS();
|
||||
|
||||
// ====================================================
|
||||
// Modifiers
|
||||
@@ -157,6 +224,16 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Modifier to restrict access to functions to only the TEE.
|
||||
* @dev Reverts if the TEE is not set or if the caller is not the TEE.
|
||||
*/
|
||||
modifier onlyTEE() {
|
||||
if (address(_tee) == address(0)) revert TEE_NOT_SET();
|
||||
if (msg.sender != address(_tee)) revert ONLY_TEE_CAN_ACCESS();
|
||||
_;
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// Constructor
|
||||
// ====================================================
|
||||
@@ -197,6 +274,25 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
__ImplRoot_init();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes OFAC proof verification infrastructure.
|
||||
* @dev Sets GCP JWT verifier, PCR0Manager, and root CA pubkey hash.
|
||||
* @param gcpJwtVerifier_ The GCP JWT Groth16 verifier address.
|
||||
* @param pcr0Manager_ The PCR0Manager address for TEE image validation.
|
||||
* @param gcpRootCAPubkeyHash_ The expected Poseidon hash of the GCP root CA public key.
|
||||
*/
|
||||
function initializeOfacProof(
|
||||
address gcpJwtVerifier_,
|
||||
address pcr0Manager_,
|
||||
uint256 gcpRootCAPubkeyHash_,
|
||||
address teeAddress_
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) reinitializer(3) {
|
||||
_gcpJwtVerifier = gcpJwtVerifier_;
|
||||
_pcr0Manager = pcr0Manager_;
|
||||
_gcpRootCAPubkeyHash = gcpRootCAPubkeyHash_;
|
||||
_tee = teeAddress_;
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// External Functions - View & Checks
|
||||
// ====================================================
|
||||
@@ -287,6 +383,32 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
return _nameAndYobOfacRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Retrieves the previous name and date of birth OFAC root (rolling window).
|
||||
* @return The stored previous name and date of birth OFAC root.
|
||||
*/
|
||||
function getPrevNameAndDobOfacRoot() external view onlyProxy returns (uint256) {
|
||||
return _prevNameAndDobOfacRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Retrieves the previous name and year of birth OFAC root (rolling window).
|
||||
* @return The stored previous name and year of birth OFAC root.
|
||||
*/
|
||||
function getPrevNameAndYobOfacRoot() external view onlyProxy returns (uint256) {
|
||||
return _prevNameAndYobOfacRoot;
|
||||
}
|
||||
|
||||
/// @notice Returns the address of the GCP JWT verifier contract.
|
||||
function getGcpJwtVerifier() external view onlyProxy returns (address) {
|
||||
return _gcpJwtVerifier;
|
||||
}
|
||||
|
||||
/// @notice Returns the address of the PCR0 Manager contract.
|
||||
function getPcr0Manager() external view onlyProxy returns (address) {
|
||||
return _pcr0Manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Validates whether the provided OFAC roots match the stored values.
|
||||
* @param nameAndDobRoot The name and date of birth OFAC root to validate.
|
||||
@@ -294,7 +416,11 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
* @return True if all provided roots match the stored values, false otherwise.
|
||||
*/
|
||||
function checkOfacRoots(uint256 nameAndDobRoot, uint256 nameAndYobRoot) external view onlyProxy returns (bool) {
|
||||
return _nameAndDobOfacRoot == nameAndDobRoot && _nameAndYobOfacRoot == nameAndYobRoot;
|
||||
bool currentMatch = (_nameAndDobOfacRoot == nameAndDobRoot) && (_nameAndYobOfacRoot == nameAndYobRoot);
|
||||
bool prevMatch = (_prevNameAndDobOfacRoot != 0) &&
|
||||
(_prevNameAndDobOfacRoot == nameAndDobRoot) &&
|
||||
(_prevNameAndYobOfacRoot == nameAndYobRoot);
|
||||
return currentMatch || prevMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,6 +533,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
* @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
|
||||
*/
|
||||
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_prevNameAndDobOfacRoot = _nameAndDobOfacRoot;
|
||||
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
|
||||
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
|
||||
}
|
||||
@@ -417,6 +544,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
* @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
|
||||
*/
|
||||
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_prevNameAndYobOfacRoot = _nameAndYobOfacRoot;
|
||||
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
|
||||
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
|
||||
}
|
||||
@@ -431,6 +559,96 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
emit CscaRootUpdated(newCscaRoot);
|
||||
}
|
||||
|
||||
/// @notice Updates the GCP JWT verifier contract address.
|
||||
/// @param verifier The new GCP JWT verifier address.
|
||||
function updateGCPJWTVerifier(address verifier) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_gcpJwtVerifier = verifier;
|
||||
emit GCPJWTVerifierUpdated(verifier);
|
||||
}
|
||||
|
||||
/// @notice Updates the PCR0Manager address.
|
||||
/// @param newPCR0Manager The new PCR0Manager address.
|
||||
function updatePCR0Manager(address newPCR0Manager) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_pcr0Manager = newPCR0Manager;
|
||||
emit PCR0ManagerUpdated(newPCR0Manager);
|
||||
}
|
||||
|
||||
/// @notice Updates the GCP root CA pubkey hash.
|
||||
/// @param newHash The new GCP root CA pubkey hash value.
|
||||
function updateGCPRootCAPubkeyHash(uint256 newHash) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_gcpRootCAPubkeyHash = newHash;
|
||||
emit GCPRootCAPubkeyHashUpdated(newHash);
|
||||
}
|
||||
|
||||
/// @notice Updates the TEE address.
|
||||
/// @param teeAddress The new TEE address.
|
||||
function updateTEE(address teeAddress) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_tee = teeAddress;
|
||||
emit TEEUpdated(teeAddress);
|
||||
}
|
||||
|
||||
/// @notice Retrieves the TEE address.
|
||||
/// @return The current TEE address.
|
||||
function tee() external view onlyProxy returns (address) {
|
||||
return _tee;
|
||||
}
|
||||
|
||||
/// @notice Updates OFAC roots via proof-verified TEE attestation.
|
||||
/// @dev Verifies the Groth16 proof, validates TEE attestation claims, checks
|
||||
/// this registry's roots hash against the eat_nonce from the proof. Restricted to the TEE address. The proof provides
|
||||
/// cryptographic verification, and onlyTEE provides access control.
|
||||
/// @param pA Groth16 proof element A.
|
||||
/// @param pB Groth16 proof element B.
|
||||
/// @param pC Groth16 proof element C.
|
||||
/// @param pubSignals Circuit public signals [rootCA, eatNonce[0-2], unused, imageHash[0-2], date[0-11]].
|
||||
/// @param roots This registry's roots: [nameAndDob, nameAndYob].
|
||||
function updateOfacRootsWithProof(
|
||||
uint256[2] calldata pA,
|
||||
uint256[2][2] calldata pB,
|
||||
uint256[2] calldata pC,
|
||||
uint256[20] calldata pubSignals,
|
||||
uint256[] calldata roots
|
||||
) external onlyProxy onlyTEE {
|
||||
if (roots.length != 2) revert InvalidRootsCount();
|
||||
|
||||
// Verify Groth16 proof
|
||||
if (!IGCPJWTVerifier(_gcpJwtVerifier).verifyProof(pA, pB, pC, pubSignals)) revert INVALID_PROOF();
|
||||
|
||||
// Verify root CA pubkey hash
|
||||
if (pubSignals[0] != _gcpRootCAPubkeyHash) revert INVALID_ROOT_CA();
|
||||
|
||||
// Verify TEE image hash
|
||||
bytes memory imageHash = GCPJWTHelper.unpackAndConvertImageHash(pubSignals[5], pubSignals[6], pubSignals[7]);
|
||||
if (!IPCR0Manager(_pcr0Manager).isPCR0Set(imageHash)) revert INVALID_IMAGE();
|
||||
|
||||
// Verify timestamp (±1 hour)
|
||||
uint256 currentTimestamp = Formatter.toTimeStampWithSeconds(
|
||||
2000 + pubSignals[8] * 10 + pubSignals[9],
|
||||
pubSignals[10] * 10 + pubSignals[11],
|
||||
pubSignals[12] * 10 + pubSignals[13],
|
||||
pubSignals[14] * 10 + pubSignals[15],
|
||||
pubSignals[16] * 10 + pubSignals[17],
|
||||
pubSignals[18] * 10 + pubSignals[19]
|
||||
);
|
||||
if (currentTimestamp + 1 hours < block.timestamp) revert INVALID_TIMESTAMP();
|
||||
if (currentTimestamp > block.timestamp + 1 hours) revert INVALID_TIMESTAMP();
|
||||
|
||||
// Verify roots hash matches eat_nonce from proof
|
||||
bytes32 myHash = sha256(abi.encodePacked(roots[0], roots[1]));
|
||||
uint256 rootsHashFromProof = GCPJWTHelper.unpackAndDecodeHexPubkey(pubSignals[1], pubSignals[2], pubSignals[3]);
|
||||
if (uint256(myHash) != rootsHashFromProof) revert InvalidRootsHash();
|
||||
|
||||
// Update this registry's roots with rolling window: [nameAndDob, nameAndYob]
|
||||
_prevNameAndDobOfacRoot = _nameAndDobOfacRoot;
|
||||
_nameAndDobOfacRoot = roots[0];
|
||||
_prevNameAndYobOfacRoot = _nameAndYobOfacRoot;
|
||||
_nameAndYobOfacRoot = roots[1];
|
||||
|
||||
emit NameAndDobOfacRootUpdated(roots[0]);
|
||||
emit NameAndYobOfacRootUpdated(roots[1]);
|
||||
emit OfacRootsUpdatedWithProof(myHash, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice (DEV) Force-adds an identity commitment.
|
||||
* @dev Callable only by the owner for testing or administration.
|
||||
|
||||
243
contracts/contracts/registry/IdentityRegistryImplV1.sol
Normal file → Executable file
243
contracts/contracts/registry/IdentityRegistryImplV1.sol
Normal file → Executable file
@@ -8,6 +8,29 @@ import {InternalLeanIMT, LeanIMTData} from "@zk-kit/imt.sol/internal/InternalLea
|
||||
import {IIdentityRegistryV1} from "../interfaces/IIdentityRegistryV1.sol";
|
||||
import {IIdentityVerificationHubV1} from "../interfaces/IIdentityVerificationHubV1.sol";
|
||||
import {ImplRoot} from "../upgradeable/ImplRoot.sol";
|
||||
import {GCPJWTHelper} from "../libraries/GCPJWTHelper.sol";
|
||||
import {Formatter} from "../libraries/Formatter.sol";
|
||||
/**
|
||||
* @title IGCPJWTVerifier
|
||||
* @notice Interface for the GCP JWT verifier contract.
|
||||
*/
|
||||
interface IGCPJWTVerifier {
|
||||
function verifyProof(
|
||||
uint256[2] calldata pA,
|
||||
uint256[2][2] calldata pB,
|
||||
uint256[2] calldata pC,
|
||||
uint256[20] calldata pubSignals
|
||||
) external view returns (bool);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title IPCR0Manager
|
||||
* @notice Interface for the PCR0 (TEE image hash) manager contract.
|
||||
*/
|
||||
interface IPCR0Manager {
|
||||
function isPCR0Set(bytes calldata pcr0) external view returns (bool);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice ⚠️ CRITICAL STORAGE LAYOUT WARNING ⚠️
|
||||
* =============================================
|
||||
@@ -76,6 +99,27 @@ abstract contract IdentityRegistryStorageV1 is ImplRoot {
|
||||
|
||||
/// @notice Current CSCA root.
|
||||
uint256 internal _cscaRoot;
|
||||
|
||||
/// @notice Previous passport number OFAC root (rolling window).
|
||||
uint256 internal _prevPassportNoOfacRoot;
|
||||
|
||||
/// @notice Previous name and date of birth OFAC root (rolling window).
|
||||
uint256 internal _prevNameAndDobOfacRoot;
|
||||
|
||||
/// @notice Previous name and year of birth OFAC root (rolling window).
|
||||
uint256 internal _prevNameAndYobOfacRoot;
|
||||
|
||||
/// @notice Address of the GCP JWT verifier contract for OFAC proof updates.
|
||||
address internal _gcpJwtVerifier;
|
||||
|
||||
/// @notice Address of the PCR0Manager for OFAC proof updates.
|
||||
address internal _pcr0Manager;
|
||||
|
||||
/// @notice Expected hash of the GCP root CA public key for OFAC proof verification.
|
||||
uint256 internal _gcpRootCAPubkeyHash;
|
||||
|
||||
/// @notice Address of the TEE authorized to call updateOfacRootsWithProof.
|
||||
address internal _tee;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +127,7 @@ abstract contract IdentityRegistryStorageV1 is ImplRoot {
|
||||
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
|
||||
* @dev Inherits from IdentityRegistryStorageV1 and implements IIdentityRegistryV1.
|
||||
*
|
||||
* @custom:version 1.2.0
|
||||
* @custom:version 1.3.1
|
||||
*/
|
||||
contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV1 {
|
||||
using InternalLeanIMT for LeanIMTData;
|
||||
@@ -138,6 +182,16 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
event DevNullifierStateChanged(bytes32 indexed attestationId, uint256 indexed nullifier, bool state);
|
||||
/// @notice Emitted when the state of a DSC key commitment is changed by dev team.
|
||||
event DevDscKeyCommitmentStateChanged(uint256 indexed commitment, bool state);
|
||||
/// @notice Emitted when OFAC roots are updated via proof.
|
||||
event OfacRootsUpdatedWithProof(bytes32 rootsHash, uint256 timestamp);
|
||||
/// @notice Emitted when the GCP JWT verifier address is updated.
|
||||
event GCPJWTVerifierUpdated(address gcpJwtVerifier);
|
||||
/// @notice Emitted when the PCR0Manager address is updated.
|
||||
event PCR0ManagerUpdated(address pcr0Manager);
|
||||
/// @notice Emitted when the GCP root CA pubkey hash is updated.
|
||||
event GCPRootCAPubkeyHashUpdated(uint256 gcpRootCAPubkeyHash);
|
||||
/// @notice Emitted when the TEE address is updated.
|
||||
event TEEUpdated(address tee);
|
||||
|
||||
// ====================================================
|
||||
// Errors
|
||||
@@ -149,6 +203,22 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
error ONLY_HUB_CAN_ACCESS();
|
||||
/// @notice Thrown when attempting to register a commitment that has already been registered.
|
||||
error REGISTERED_COMMITMENT();
|
||||
/// @notice Thrown when the GCP JWT proof verification fails.
|
||||
error INVALID_PROOF();
|
||||
/// @notice Thrown when the GCP root CA public key hash does not match the expected value.
|
||||
error INVALID_ROOT_CA();
|
||||
/// @notice Thrown when the TEE image hash is not registered in the PCR0Manager.
|
||||
error INVALID_IMAGE();
|
||||
/// @notice Thrown when the timestamp is invalid.
|
||||
error INVALID_TIMESTAMP();
|
||||
/// @notice Thrown when the roots hash does not match the proof.
|
||||
error InvalidRootsHash();
|
||||
/// @notice Thrown when the wrong number of roots is provided.
|
||||
error InvalidRootsCount();
|
||||
/// @notice Thrown when the TEE address is not set.
|
||||
error TEE_NOT_SET();
|
||||
/// @notice Thrown when a function is accessed by an address other than the designated TEE.
|
||||
error ONLY_TEE_CAN_ACCESS();
|
||||
|
||||
// ====================================================
|
||||
// Modifiers
|
||||
@@ -164,6 +234,16 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Modifier to restrict access to functions to only the TEE.
|
||||
* @dev Reverts if the TEE is not set or if the caller is not the TEE.
|
||||
*/
|
||||
modifier onlyTEE() {
|
||||
if (address(_tee) == address(0)) revert TEE_NOT_SET();
|
||||
if (msg.sender != address(_tee)) revert ONLY_TEE_CAN_ACCESS();
|
||||
_;
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// Constructor
|
||||
// ====================================================
|
||||
@@ -204,6 +284,25 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
__ImplRoot_init();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes OFAC proof verification infrastructure.
|
||||
* @dev Sets GCP JWT verifier, PCR0Manager, and root CA pubkey hash.
|
||||
* @param gcpJwtVerifier_ The GCP JWT Groth16 verifier address.
|
||||
* @param pcr0Manager_ The PCR0Manager address for TEE image validation.
|
||||
* @param gcpRootCAPubkeyHash_ The expected Poseidon hash of the GCP root CA public key.
|
||||
*/
|
||||
function initializeOfacProof(
|
||||
address gcpJwtVerifier_,
|
||||
address pcr0Manager_,
|
||||
uint256 gcpRootCAPubkeyHash_,
|
||||
address teeAddress_
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) reinitializer(3) {
|
||||
_gcpJwtVerifier = gcpJwtVerifier_;
|
||||
_pcr0Manager = pcr0Manager_;
|
||||
_gcpRootCAPubkeyHash = gcpRootCAPubkeyHash_;
|
||||
_tee = teeAddress_;
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// External Functions - View & Checks
|
||||
// ====================================================
|
||||
@@ -302,6 +401,40 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
return _nameAndYobOfacRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Retrieves the previous passport number OFAC root (rolling window).
|
||||
* @return The stored previous passport number OFAC root.
|
||||
*/
|
||||
function getPrevPassportNoOfacRoot() external view onlyProxy returns (uint256) {
|
||||
return _prevPassportNoOfacRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Retrieves the previous name and date of birth OFAC root (rolling window).
|
||||
* @return The stored previous name and date of birth OFAC root.
|
||||
*/
|
||||
function getPrevNameAndDobOfacRoot() external view onlyProxy returns (uint256) {
|
||||
return _prevNameAndDobOfacRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Retrieves the previous name and year of birth OFAC root (rolling window).
|
||||
* @return The stored previous name and year of birth OFAC root.
|
||||
*/
|
||||
function getPrevNameAndYobOfacRoot() external view onlyProxy returns (uint256) {
|
||||
return _prevNameAndYobOfacRoot;
|
||||
}
|
||||
|
||||
/// @notice Returns the address of the GCP JWT verifier contract.
|
||||
function getGcpJwtVerifier() external view onlyProxy returns (address) {
|
||||
return _gcpJwtVerifier;
|
||||
}
|
||||
|
||||
/// @notice Returns the address of the PCR0 Manager contract.
|
||||
function getPcr0Manager() external view onlyProxy returns (address) {
|
||||
return _pcr0Manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Validates whether the provided OFAC roots match the stored values.
|
||||
* @param passportNoRoot The passport number OFAC root to validate.
|
||||
@@ -314,10 +447,14 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
uint256 nameAndDobRoot,
|
||||
uint256 nameAndYobRoot
|
||||
) external view onlyProxy returns (bool) {
|
||||
return
|
||||
_passportNoOfacRoot == passportNoRoot &&
|
||||
_nameAndDobOfacRoot == nameAndDobRoot &&
|
||||
_nameAndYobOfacRoot == nameAndYobRoot;
|
||||
bool currentMatch = (_passportNoOfacRoot == passportNoRoot) &&
|
||||
(_nameAndDobOfacRoot == nameAndDobRoot) &&
|
||||
(_nameAndYobOfacRoot == nameAndYobRoot);
|
||||
bool prevMatch = (_prevPassportNoOfacRoot != 0) &&
|
||||
(_prevPassportNoOfacRoot == passportNoRoot) &&
|
||||
(_prevNameAndDobOfacRoot == nameAndDobRoot) &&
|
||||
(_prevNameAndYobOfacRoot == nameAndYobRoot);
|
||||
return currentMatch || prevMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -430,6 +567,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
* @param newPassportNoOfacRoot The new passport number OFAC root value.
|
||||
*/
|
||||
function updatePassportNoOfacRoot(uint256 newPassportNoOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_prevPassportNoOfacRoot = _passportNoOfacRoot;
|
||||
_passportNoOfacRoot = newPassportNoOfacRoot;
|
||||
emit PassportNoOfacRootUpdated(newPassportNoOfacRoot);
|
||||
}
|
||||
@@ -440,6 +578,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
* @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
|
||||
*/
|
||||
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_prevNameAndDobOfacRoot = _nameAndDobOfacRoot;
|
||||
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
|
||||
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
|
||||
}
|
||||
@@ -450,6 +589,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
* @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
|
||||
*/
|
||||
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_prevNameAndYobOfacRoot = _nameAndYobOfacRoot;
|
||||
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
|
||||
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
|
||||
}
|
||||
@@ -464,6 +604,99 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
emit CscaRootUpdated(newCscaRoot);
|
||||
}
|
||||
|
||||
/// @notice Updates the GCP JWT verifier contract address.
|
||||
/// @param verifier The new GCP JWT verifier address.
|
||||
function updateGCPJWTVerifier(address verifier) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_gcpJwtVerifier = verifier;
|
||||
emit GCPJWTVerifierUpdated(verifier);
|
||||
}
|
||||
|
||||
/// @notice Updates the PCR0Manager address.
|
||||
/// @param newPCR0Manager The new PCR0Manager address.
|
||||
function updatePCR0Manager(address newPCR0Manager) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_pcr0Manager = newPCR0Manager;
|
||||
emit PCR0ManagerUpdated(newPCR0Manager);
|
||||
}
|
||||
|
||||
/// @notice Updates the GCP root CA pubkey hash.
|
||||
/// @param newHash The new GCP root CA pubkey hash value.
|
||||
function updateGCPRootCAPubkeyHash(uint256 newHash) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_gcpRootCAPubkeyHash = newHash;
|
||||
emit GCPRootCAPubkeyHashUpdated(newHash);
|
||||
}
|
||||
|
||||
/// @notice Updates the TEE address.
|
||||
/// @param teeAddress The new TEE address.
|
||||
function updateTEE(address teeAddress) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_tee = teeAddress;
|
||||
emit TEEUpdated(teeAddress);
|
||||
}
|
||||
|
||||
/// @notice Retrieves the TEE address.
|
||||
/// @return The current TEE address.
|
||||
function tee() external view onlyProxy returns (address) {
|
||||
return _tee;
|
||||
}
|
||||
|
||||
/// @notice Updates OFAC roots via proof-verified TEE attestation.
|
||||
/// @dev Verifies the Groth16 proof, validates TEE attestation claims, checks
|
||||
/// this registry's roots hash against the eat_nonce from the proof. Restricted to the TEE address. The proof provides
|
||||
/// cryptographic verification, and onlyTEE provides access control.
|
||||
/// @param pA Groth16 proof element A.
|
||||
/// @param pB Groth16 proof element B.
|
||||
/// @param pC Groth16 proof element C.
|
||||
/// @param pubSignals Circuit public signals [rootCA, eatNonce[0-2], unused, imageHash[0-2], date[0-11]].
|
||||
/// @param roots This registry's roots: [nameAndDob, nameAndYob, passportNo].
|
||||
function updateOfacRootsWithProof(
|
||||
uint256[2] calldata pA,
|
||||
uint256[2][2] calldata pB,
|
||||
uint256[2] calldata pC,
|
||||
uint256[20] calldata pubSignals,
|
||||
uint256[] calldata roots
|
||||
) external onlyProxy onlyTEE {
|
||||
if (roots.length != 3) revert InvalidRootsCount();
|
||||
|
||||
// Verify Groth16 proof
|
||||
if (!IGCPJWTVerifier(_gcpJwtVerifier).verifyProof(pA, pB, pC, pubSignals)) revert INVALID_PROOF();
|
||||
|
||||
// Verify root CA pubkey hash
|
||||
if (pubSignals[0] != _gcpRootCAPubkeyHash) revert INVALID_ROOT_CA();
|
||||
|
||||
// Verify TEE image hash
|
||||
bytes memory imageHash = GCPJWTHelper.unpackAndConvertImageHash(pubSignals[5], pubSignals[6], pubSignals[7]);
|
||||
if (!IPCR0Manager(_pcr0Manager).isPCR0Set(imageHash)) revert INVALID_IMAGE();
|
||||
|
||||
// Verify timestamp (±1 hour)
|
||||
uint256 currentTimestamp = Formatter.toTimeStampWithSeconds(
|
||||
2000 + pubSignals[8] * 10 + pubSignals[9],
|
||||
pubSignals[10] * 10 + pubSignals[11],
|
||||
pubSignals[12] * 10 + pubSignals[13],
|
||||
pubSignals[14] * 10 + pubSignals[15],
|
||||
pubSignals[16] * 10 + pubSignals[17],
|
||||
pubSignals[18] * 10 + pubSignals[19]
|
||||
);
|
||||
if (currentTimestamp + 1 hours < block.timestamp) revert INVALID_TIMESTAMP();
|
||||
if (currentTimestamp > block.timestamp + 1 hours) revert INVALID_TIMESTAMP();
|
||||
|
||||
// Verify roots hash matches eat_nonce from proof
|
||||
bytes32 myHash = sha256(abi.encodePacked(roots[0], roots[1], roots[2]));
|
||||
uint256 rootsHashFromProof = GCPJWTHelper.unpackAndDecodeHexPubkey(pubSignals[1], pubSignals[2], pubSignals[3]);
|
||||
if (uint256(myHash) != rootsHashFromProof) revert InvalidRootsHash();
|
||||
|
||||
// Update this registry's roots with rolling window: [nameAndDob, nameAndYob, passportNo]
|
||||
_prevNameAndDobOfacRoot = _nameAndDobOfacRoot;
|
||||
_nameAndDobOfacRoot = roots[0];
|
||||
_prevNameAndYobOfacRoot = _nameAndYobOfacRoot;
|
||||
_nameAndYobOfacRoot = roots[1];
|
||||
_prevPassportNoOfacRoot = _passportNoOfacRoot;
|
||||
_passportNoOfacRoot = roots[2];
|
||||
|
||||
emit NameAndDobOfacRootUpdated(roots[0]);
|
||||
emit NameAndYobOfacRootUpdated(roots[1]);
|
||||
emit PassportNoOfacRootUpdated(roots[2]);
|
||||
emit OfacRootsUpdatedWithProof(myHash, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice (DEV) Force-adds an identity commitment.
|
||||
* @dev Callable only by the owner for testing or administration.
|
||||
|
||||
104
contracts/contracts/registry/IdentityRegistryKycImplV1.sol
Normal file → Executable file
104
contracts/contracts/registry/IdentityRegistryKycImplV1.sol
Normal file → Executable file
@@ -73,6 +73,12 @@ abstract contract IdentityRegistryKycStorageV1 is ImplRoot {
|
||||
|
||||
/// @notice The expected hash of the GCP root CA public key for JWT verification.
|
||||
uint256 internal _gcpRootCAPubkeyHash;
|
||||
|
||||
/// @notice Previous name and date of birth OFAC root (rolling window).
|
||||
uint256 internal _prevNameAndDobOfacRoot;
|
||||
|
||||
/// @notice Previous name and year of birth OFAC root (rolling window).
|
||||
uint256 internal _prevNameAndYobOfacRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,6 +119,8 @@ interface IPCR0Manager {
|
||||
* @title IdentityRegistryKycImplV1
|
||||
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
|
||||
* @dev Inherits from IdentityRegistryKycStorageV1 and implements IIdentityRegistryKycV1.
|
||||
*
|
||||
* @custom:version 1.2.1
|
||||
*/
|
||||
contract IdentityRegistryKycImplV1 is IdentityRegistryKycStorageV1, IIdentityRegistryKycV1 {
|
||||
using InternalLeanIMT for LeanIMTData;
|
||||
@@ -148,6 +156,8 @@ contract IdentityRegistryKycImplV1 is IdentityRegistryKycStorageV1, IIdentityReg
|
||||
);
|
||||
/// @notice Emitted when a public key commitment is successfully registered.
|
||||
event PubkeyCommitmentRegistered(uint256 indexed commitment);
|
||||
/// @notice Emitted when OFAC roots are updated via proof.
|
||||
event OfacRootsUpdatedWithProof(bytes32 rootsHash, uint256 timestamp);
|
||||
|
||||
/// @notice Emitted when a identity commitment is added by dev team.
|
||||
event DevCommitmentRegistered(
|
||||
@@ -187,6 +197,10 @@ contract IdentityRegistryKycImplV1 is IdentityRegistryKycStorageV1, IIdentityReg
|
||||
error INVALID_IMAGE();
|
||||
/// @notice Thrown when the timestamp is invalid.
|
||||
error INVALID_TIMESTAMP();
|
||||
/// @notice Thrown when the roots hash does not match the proof.
|
||||
error InvalidRootsHash();
|
||||
/// @notice Thrown when the wrong number of roots is provided.
|
||||
error InvalidRootsCount();
|
||||
|
||||
// ====================================================
|
||||
// Modifiers
|
||||
@@ -333,6 +347,32 @@ contract IdentityRegistryKycImplV1 is IdentityRegistryKycStorageV1, IIdentityReg
|
||||
return _nameAndYobOfacRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Retrieves the previous name and date of birth OFAC root (rolling window).
|
||||
* @return The stored previous name and date of birth OFAC root.
|
||||
*/
|
||||
function getPrevNameAndDobOfacRoot() external view onlyProxy returns (uint256) {
|
||||
return _prevNameAndDobOfacRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Retrieves the previous name and year of birth OFAC root (rolling window).
|
||||
* @return The stored previous name and year of birth OFAC root.
|
||||
*/
|
||||
function getPrevNameAndYobOfacRoot() external view onlyProxy returns (uint256) {
|
||||
return _prevNameAndYobOfacRoot;
|
||||
}
|
||||
|
||||
/// @notice Returns the address of the GCP JWT verifier contract.
|
||||
function getGcpJwtVerifier() external view onlyProxy returns (address) {
|
||||
return _gcpJwtVerifier;
|
||||
}
|
||||
|
||||
/// @notice Returns the address of the PCR0 Manager contract.
|
||||
function getPcr0Manager() external view onlyProxy returns (address) {
|
||||
return _PCR0Manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Checks if the provided OFAC roots match the stored OFAC roots.
|
||||
* @param nameAndDobRoot The name and date of birth OFAC root to verify.
|
||||
@@ -340,7 +380,11 @@ contract IdentityRegistryKycImplV1 is IdentityRegistryKycStorageV1, IIdentityReg
|
||||
* @return True if both provided roots match the stored values, false otherwise.
|
||||
*/
|
||||
function checkOfacRoots(uint256 nameAndDobRoot, uint256 nameAndYobRoot) external view onlyProxy returns (bool) {
|
||||
return _nameAndDobOfacRoot == nameAndDobRoot && _nameAndYobOfacRoot == nameAndYobRoot;
|
||||
bool currentMatch = (_nameAndDobOfacRoot == nameAndDobRoot) && (_nameAndYobOfacRoot == nameAndYobRoot);
|
||||
bool prevMatch = (_prevNameAndDobOfacRoot != 0) &&
|
||||
(_prevNameAndDobOfacRoot == nameAndDobRoot) &&
|
||||
(_prevNameAndYobOfacRoot == nameAndYobRoot);
|
||||
return currentMatch || prevMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -400,6 +444,7 @@ contract IdentityRegistryKycImplV1 is IdentityRegistryKycStorageV1, IIdentityReg
|
||||
* @param nameAndDobOfacRoot The new name and date of birth OFAC root value.
|
||||
*/
|
||||
function updateNameAndDobOfacRoot(uint256 nameAndDobOfacRoot) external virtual onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_prevNameAndDobOfacRoot = _nameAndDobOfacRoot;
|
||||
_nameAndDobOfacRoot = nameAndDobOfacRoot;
|
||||
emit NameAndDobOfacRootUpdated(nameAndDobOfacRoot);
|
||||
}
|
||||
@@ -410,6 +455,7 @@ contract IdentityRegistryKycImplV1 is IdentityRegistryKycStorageV1, IIdentityReg
|
||||
* @param nameAndYobOfacRoot The new name and year of birth OFAC root value.
|
||||
*/
|
||||
function updateNameAndYobOfacRoot(uint256 nameAndYobOfacRoot) external virtual onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_prevNameAndYobOfacRoot = _nameAndYobOfacRoot;
|
||||
_nameAndYobOfacRoot = nameAndYobOfacRoot;
|
||||
emit NameAndYobOfacRootUpdated(nameAndYobOfacRoot);
|
||||
}
|
||||
@@ -487,6 +533,62 @@ contract IdentityRegistryKycImplV1 is IdentityRegistryKycStorageV1, IIdentityReg
|
||||
emit PubkeyCommitmentRegistered(pubkeyCommitment);
|
||||
}
|
||||
|
||||
/// @notice Updates OFAC roots via proof-verified TEE attestation.
|
||||
/// @dev Verifies the Groth16 proof, validates TEE attestation claims, checks
|
||||
/// this registry's roots hash against the eat_nonce from the proof. Restricted to the TEE address. The proof provides
|
||||
/// cryptographic verification, and onlyTEE provides access control.
|
||||
/// @param pA Groth16 proof element A.
|
||||
/// @param pB Groth16 proof element B.
|
||||
/// @param pC Groth16 proof element C.
|
||||
/// @param pubSignals Circuit public signals [rootCA, eatNonce[0-2], unused, imageHash[0-2], date[0-11]].
|
||||
/// @param roots This registry's roots: [nameAndDob, nameAndYob].
|
||||
function updateOfacRootsWithProof(
|
||||
uint256[2] calldata pA,
|
||||
uint256[2][2] calldata pB,
|
||||
uint256[2] calldata pC,
|
||||
uint256[20] calldata pubSignals,
|
||||
uint256[] calldata roots
|
||||
) external onlyProxy onlyTEE {
|
||||
if (roots.length != 2) revert InvalidRootsCount();
|
||||
|
||||
// Verify Groth16 proof
|
||||
if (!IGCPJWTVerifier(_gcpJwtVerifier).verifyProof(pA, pB, pC, pubSignals)) revert INVALID_PROOF();
|
||||
|
||||
// Verify root CA pubkey hash
|
||||
if (pubSignals[0] != _gcpRootCAPubkeyHash) revert INVALID_ROOT_CA();
|
||||
|
||||
// Verify TEE image hash
|
||||
bytes memory imageHash = GCPJWTHelper.unpackAndConvertImageHash(pubSignals[5], pubSignals[6], pubSignals[7]);
|
||||
if (!IPCR0Manager(_PCR0Manager).isPCR0Set(imageHash)) revert INVALID_IMAGE();
|
||||
|
||||
// Verify timestamp (±1 hour)
|
||||
uint256 currentTimestamp = Formatter.toTimeStampWithSeconds(
|
||||
2000 + pubSignals[8] * 10 + pubSignals[9],
|
||||
pubSignals[10] * 10 + pubSignals[11],
|
||||
pubSignals[12] * 10 + pubSignals[13],
|
||||
pubSignals[14] * 10 + pubSignals[15],
|
||||
pubSignals[16] * 10 + pubSignals[17],
|
||||
pubSignals[18] * 10 + pubSignals[19]
|
||||
);
|
||||
if (currentTimestamp + 1 hours < block.timestamp) revert INVALID_TIMESTAMP();
|
||||
if (currentTimestamp > block.timestamp + 1 hours) revert INVALID_TIMESTAMP();
|
||||
|
||||
// Verify roots hash matches eat_nonce from proof
|
||||
bytes32 myHash = sha256(abi.encodePacked(roots[0], roots[1]));
|
||||
uint256 rootsHashFromProof = GCPJWTHelper.unpackAndDecodeHexPubkey(pubSignals[1], pubSignals[2], pubSignals[3]);
|
||||
if (uint256(myHash) != rootsHashFromProof) revert InvalidRootsHash();
|
||||
|
||||
// Update this registry's roots: [nameAndDob, nameAndYob]
|
||||
_prevNameAndDobOfacRoot = _nameAndDobOfacRoot;
|
||||
_prevNameAndYobOfacRoot = _nameAndYobOfacRoot;
|
||||
_nameAndDobOfacRoot = roots[0];
|
||||
_nameAndYobOfacRoot = roots[1];
|
||||
|
||||
emit NameAndDobOfacRootUpdated(roots[0]);
|
||||
emit NameAndYobOfacRootUpdated(roots[1]);
|
||||
emit OfacRootsUpdatedWithProof(myHash, block.timestamp);
|
||||
}
|
||||
|
||||
/// @notice (DEV) Force-adds an identity commitment.
|
||||
/// @dev Callable only by the owner for testing or administration.
|
||||
/// @param nullifier The nullifier associated with the identity commitment.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "./registry.schema.json",
|
||||
"lastUpdated": "2026-02-09T11:26:31.105Z",
|
||||
"lastUpdated": "2026-03-02T10:19:46.004Z",
|
||||
"contracts": {
|
||||
"IdentityVerificationHub": {
|
||||
"source": "IdentityVerificationHubImplV2",
|
||||
@@ -96,6 +96,30 @@
|
||||
"proxy": "0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74",
|
||||
"currentVersion": "2.13.0",
|
||||
"currentImpl": "0x244c93516Abd58E1952452d3D8C4Ce7D454776B8"
|
||||
},
|
||||
"IdentityRegistry": {
|
||||
"proxy": "0x1651ec77c3dC5997eC05f3EE6C2B0b904b516d1d",
|
||||
"currentVersion": "1.2.0",
|
||||
"currentImpl": "0x873b1289b69C452Fd8349DbAfc748183eB5314ec"
|
||||
},
|
||||
"IdentityRegistryIdCard": {
|
||||
"proxy": "0x6B39222c3b98003010695cE0A31C9b1a61e07DdC",
|
||||
"currentVersion": "1.2.0",
|
||||
"currentImpl": "0xF4781c7e801D1E49aa3A95537FaEF7718f4499Cd"
|
||||
},
|
||||
"IdentityRegistryAadhaar": {
|
||||
"proxy": "0x9cbB71468f93672DBF50f511c038eAF9fAB04732",
|
||||
"currentVersion": "1.2.0",
|
||||
"currentImpl": "0x74A2848D945eCffeE325dAbc9E0b72c118fAD327"
|
||||
},
|
||||
"IdentityRegistryKyc": {
|
||||
"proxy": "0x90e907E4AaB6e9bcFB94997Af4A097e8CAadBdf3",
|
||||
"currentVersion": "1.2.0",
|
||||
"currentImpl": "0x6E2889Bc9baa6F53bDdf4843675155811F0AAAEd"
|
||||
},
|
||||
"PCR0Manager": {
|
||||
"address": "0xf2810D5E9938816D42F0Ae69D33F013a23C0aED2",
|
||||
"currentVersion": "1.2.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -152,20 +176,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"2.13.0": {
|
||||
"initializerVersion": 12,
|
||||
"initializerFunction": "",
|
||||
"changelog": "Upgrade to v2.13.0",
|
||||
"gitTag": "identityverificationhub-v2.13.0",
|
||||
"deployments": {
|
||||
"celo-sepolia": {
|
||||
"impl": "0x244c93516Abd58E1952452d3D8C4Ce7D454776B8",
|
||||
"deployedAt": "2026-02-02T14:47:21.882Z",
|
||||
"deployedBy": "0x82D8DaC3a386dec55a0a44DffBd3113e8A7D139B",
|
||||
"gitCommit": "33bca485"
|
||||
}
|
||||
}
|
||||
},
|
||||
"2.13.0": {
|
||||
"initializerVersion": 12,
|
||||
"initializerFunction": "",
|
||||
@@ -193,6 +203,12 @@
|
||||
"deployedAt": "2025-12-10T05:53:12.534Z",
|
||||
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||
"gitCommit": ""
|
||||
},
|
||||
"celo-sepolia": {
|
||||
"impl": "0x873b1289b69C452Fd8349DbAfc748183eB5314ec",
|
||||
"deployedAt": "2025-09-16T04:37:22.000Z",
|
||||
"deployedBy": "0x82D8DaC3a386dec55a0a44DffBd3113e8A7D139B",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -223,6 +239,12 @@
|
||||
"deployedAt": "2025-12-10T05:45:56.772Z",
|
||||
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||
"gitCommit": ""
|
||||
},
|
||||
"celo-sepolia": {
|
||||
"impl": "0xF4781c7e801D1E49aa3A95537FaEF7718f4499Cd",
|
||||
"deployedAt": "2025-09-16T04:38:58.000Z",
|
||||
"deployedBy": "0x82D8DaC3a386dec55a0a44DffBd3113e8A7D139B",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -253,6 +275,12 @@
|
||||
"deployedAt": "2025-12-10T05:47:22.844Z",
|
||||
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||
"gitCommit": ""
|
||||
},
|
||||
"celo-sepolia": {
|
||||
"impl": "0x74A2848D945eCffeE325dAbc9E0b72c118fAD327",
|
||||
"deployedAt": "2025-09-16T04:41:01.000Z",
|
||||
"deployedBy": "0x82D8DaC3a386dec55a0a44DffBd3113e8A7D139B",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -283,6 +311,12 @@
|
||||
"deployedAt": "2025-12-10T06:17:50.863Z",
|
||||
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||
"gitCommit": "5787cff3bcbea870b50eccd7164fbd45b758568e"
|
||||
},
|
||||
"celo-sepolia": {
|
||||
"impl": "0xf2810D5E9938816D42F0Ae69D33F013a23C0aED2",
|
||||
"deployedAt": "2026-02-01T08:26:56.000Z",
|
||||
"deployedBy": "0x82D8DaC3a386dec55a0a44DffBd3113e8A7D139B",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,6 +333,40 @@
|
||||
"deployedAt": "2026-02-09T00:00:00.000Z",
|
||||
"deployedBy": "",
|
||||
"gitCommit": "03876a86284b0ed794fbff7aae142e62a3212624"
|
||||
},
|
||||
"celo-sepolia": {
|
||||
"impl": "0x94f6DE38E10140B9E3963a770B5B769b38459a3B",
|
||||
"deployedAt": "2026-02-01T08:27:21.000Z",
|
||||
"deployedBy": "0x82D8DaC3a386dec55a0a44DffBd3113e8A7D139B",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"1.1.0": {
|
||||
"initializerVersion": 1,
|
||||
"initializerFunction": "",
|
||||
"changelog": "Add TEE-attested OFAC root updates via updateOfacRootsWithProof",
|
||||
"gitTag": "identityregistrykyc-v1.1.0",
|
||||
"deployments": {
|
||||
"celo-sepolia": {
|
||||
"impl": "0x530eEA7E5b286108926B05510491560c4bAE018e",
|
||||
"deployedAt": "2026-03-02T05:00:49.070Z",
|
||||
"deployedBy": "0xC1C860804EFdA544fe79194d1a37e60b846CEdeb",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"1.2.0": {
|
||||
"initializerVersion": 1,
|
||||
"initializerFunction": "",
|
||||
"changelog": "Add rolling OFAC root window: store previous roots alongside current, accept either in checkOfacRoots for graceful mid-verification transitions",
|
||||
"gitTag": "identityregistrykyc-v1.2.0",
|
||||
"deployments": {
|
||||
"celo-sepolia": {
|
||||
"impl": "0x6E2889Bc9baa6F53bDdf4843675155811F0AAAEd",
|
||||
"deployedAt": "2026-03-02T10:19:45.990Z",
|
||||
"deployedBy": "0x82D8DaC3a386dec55a0a44DffBd3113e8A7D139B",
|
||||
"gitCommit": "611c30d21"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,28 @@
|
||||
"signature": "REGISTERED_COMMITMENT()",
|
||||
"selector": "0x034acfcc",
|
||||
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
|
||||
"line": 134
|
||||
"line": 187
|
||||
},
|
||||
{
|
||||
"name": "REGISTERED_COMMITMENT",
|
||||
"signature": "REGISTERED_COMMITMENT()",
|
||||
"selector": "0x034acfcc",
|
||||
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
|
||||
"line": 142
|
||||
"line": 195
|
||||
},
|
||||
{
|
||||
"name": "REGISTERED_COMMITMENT",
|
||||
"signature": "REGISTERED_COMMITMENT()",
|
||||
"selector": "0x034acfcc",
|
||||
"file": "contracts/registry/IdentityRegistryImplV1.sol",
|
||||
"line": 149
|
||||
"line": 205
|
||||
},
|
||||
{
|
||||
"name": "REGISTERED_COMMITMENT",
|
||||
"signature": "REGISTERED_COMMITMENT()",
|
||||
"selector": "0x034acfcc",
|
||||
"file": "contracts/registry/IdentityRegistryKycImplV1.sol",
|
||||
"line": 189
|
||||
},
|
||||
{
|
||||
"name": "InvalidProof",
|
||||
@@ -27,19 +34,103 @@
|
||||
"file": "contracts/example/Airdrop.sol",
|
||||
"line": 57
|
||||
},
|
||||
{
|
||||
"name": "InvalidProof",
|
||||
"signature": "InvalidProof()",
|
||||
"selector": "0x09bde339",
|
||||
"file": "contracts/tests/TestAirdrop.sol",
|
||||
"line": 35
|
||||
},
|
||||
{
|
||||
"name": "InvalidPubSignalsLength",
|
||||
"signature": "InvalidPubSignalsLength(uint256,uint256)",
|
||||
"selector": "0x0b42b970",
|
||||
"file": "contracts/libraries/RegisterProofVerifierLib.sol",
|
||||
"line": 44
|
||||
},
|
||||
{
|
||||
"name": "NoVerifierSet",
|
||||
"signature": "NoVerifierSet()",
|
||||
"selector": "0x0ee78d58",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 139
|
||||
"line": 151
|
||||
},
|
||||
{
|
||||
"name": "NoVerifierSet",
|
||||
"signature": "NoVerifierSet()",
|
||||
"selector": "0x0ee78d58",
|
||||
"file": "contracts/libraries/DscProofVerifierLib.sol",
|
||||
"line": 17
|
||||
},
|
||||
{
|
||||
"name": "NoVerifierSet",
|
||||
"signature": "NoVerifierSet()",
|
||||
"selector": "0x0ee78d58",
|
||||
"file": "contracts/libraries/RegisterProofVerifierLib.sol",
|
||||
"line": 23
|
||||
},
|
||||
{
|
||||
"name": "INVALID_TIMESTAMP",
|
||||
"signature": "INVALID_TIMESTAMP()",
|
||||
"selector": "0x118818d1",
|
||||
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
|
||||
"line": 197
|
||||
},
|
||||
{
|
||||
"name": "INVALID_TIMESTAMP",
|
||||
"signature": "INVALID_TIMESTAMP()",
|
||||
"selector": "0x118818d1",
|
||||
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
|
||||
"line": 203
|
||||
},
|
||||
{
|
||||
"name": "INVALID_TIMESTAMP",
|
||||
"signature": "INVALID_TIMESTAMP()",
|
||||
"selector": "0x118818d1",
|
||||
"file": "contracts/registry/IdentityRegistryImplV1.sol",
|
||||
"line": 213
|
||||
},
|
||||
{
|
||||
"name": "INVALID_TIMESTAMP",
|
||||
"signature": "INVALID_TIMESTAMP()",
|
||||
"selector": "0x118818d1",
|
||||
"file": "contracts/registry/IdentityRegistryKycImplV1.sol",
|
||||
"line": 199
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsCount",
|
||||
"signature": "InvalidRootsCount()",
|
||||
"selector": "0x128781c2",
|
||||
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
|
||||
"line": 201
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsCount",
|
||||
"signature": "InvalidRootsCount()",
|
||||
"selector": "0x128781c2",
|
||||
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
|
||||
"line": 207
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsCount",
|
||||
"signature": "InvalidRootsCount()",
|
||||
"selector": "0x128781c2",
|
||||
"file": "contracts/registry/IdentityRegistryImplV1.sol",
|
||||
"line": 217
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsCount",
|
||||
"signature": "InvalidRootsCount()",
|
||||
"selector": "0x128781c2",
|
||||
"file": "contracts/registry/IdentityRegistryKycImplV1.sol",
|
||||
"line": 203
|
||||
},
|
||||
{
|
||||
"name": "InvalidAttestationId",
|
||||
"signature": "InvalidAttestationId()",
|
||||
"selector": "0x12ec75fe",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 171
|
||||
"line": 183
|
||||
},
|
||||
{
|
||||
"name": "InvalidAttestationId",
|
||||
@@ -48,6 +139,41 @@
|
||||
"file": "contracts/libraries/CustomVerifier.sol",
|
||||
"line": 10
|
||||
},
|
||||
{
|
||||
"name": "InvalidAttestationId",
|
||||
"signature": "InvalidAttestationId()",
|
||||
"selector": "0x12ec75fe",
|
||||
"file": "contracts/libraries/DscProofVerifierLib.sol",
|
||||
"line": 26
|
||||
},
|
||||
{
|
||||
"name": "InvalidAttestationId",
|
||||
"signature": "InvalidAttestationId()",
|
||||
"selector": "0x12ec75fe",
|
||||
"file": "contracts/libraries/OfacCheckLib.sol",
|
||||
"line": 22
|
||||
},
|
||||
{
|
||||
"name": "InvalidAttestationId",
|
||||
"signature": "InvalidAttestationId()",
|
||||
"selector": "0x12ec75fe",
|
||||
"file": "contracts/libraries/ProofVerifierLib.sol",
|
||||
"line": 21
|
||||
},
|
||||
{
|
||||
"name": "InvalidAttestationId",
|
||||
"signature": "InvalidAttestationId()",
|
||||
"selector": "0x12ec75fe",
|
||||
"file": "contracts/libraries/RegisterProofVerifierLib.sol",
|
||||
"line": 32
|
||||
},
|
||||
{
|
||||
"name": "InvalidAttestationId",
|
||||
"signature": "InvalidAttestationId()",
|
||||
"selector": "0x12ec75fe",
|
||||
"file": "contracts/libraries/RootCheckLib.sol",
|
||||
"line": 22
|
||||
},
|
||||
{
|
||||
"name": "RegistrationNotOpen",
|
||||
"signature": "RegistrationNotOpen()",
|
||||
@@ -55,40 +181,61 @@
|
||||
"file": "contracts/example/Airdrop.sol",
|
||||
"line": 66
|
||||
},
|
||||
{
|
||||
"name": "RegistrationNotOpen",
|
||||
"signature": "RegistrationNotOpen()",
|
||||
"selector": "0x153745d3",
|
||||
"file": "contracts/tests/TestAirdrop.sol",
|
||||
"line": 38
|
||||
},
|
||||
{
|
||||
"name": "InvalidDscProof",
|
||||
"signature": "InvalidDscProof()",
|
||||
"selector": "0x1644e049",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 151
|
||||
"line": 163
|
||||
},
|
||||
{
|
||||
"name": "InvalidDscProof",
|
||||
"signature": "InvalidDscProof()",
|
||||
"selector": "0x1644e049",
|
||||
"file": "contracts/libraries/DscProofVerifierLib.sol",
|
||||
"line": 20
|
||||
},
|
||||
{
|
||||
"name": "InvalidYearRange",
|
||||
"signature": "InvalidYearRange()",
|
||||
"selector": "0x16f40c94",
|
||||
"file": "contracts/libraries/Formatter.sol",
|
||||
"line": 10
|
||||
"line": 12
|
||||
},
|
||||
{
|
||||
"name": "InvalidDateDigit",
|
||||
"signature": "InvalidDateDigit()",
|
||||
"selector": "0x17af8154",
|
||||
"file": "contracts/libraries/Formatter.sol",
|
||||
"line": 14
|
||||
"line": 16
|
||||
},
|
||||
{
|
||||
"name": "INVALID_OFAC_ROOT",
|
||||
"signature": "INVALID_OFAC_ROOT()",
|
||||
"selector": "0x1ce3d3ca",
|
||||
"file": "contracts/IdentityVerificationHubImplV1.sol",
|
||||
"line": 166
|
||||
"line": 176
|
||||
},
|
||||
{
|
||||
"name": "HUB_ADDRESS_ZERO",
|
||||
"signature": "HUB_ADDRESS_ZERO()",
|
||||
"selector": "0x22697ffa",
|
||||
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
|
||||
"line": 136
|
||||
"line": 189
|
||||
},
|
||||
{
|
||||
"name": "HUB_ADDRESS_ZERO",
|
||||
"signature": "HUB_ADDRESS_ZERO()",
|
||||
"selector": "0x22697ffa",
|
||||
"file": "contracts/registry/IdentityRegistryKycImplV1.sol",
|
||||
"line": 191
|
||||
},
|
||||
{
|
||||
"name": "RegisteredNullifier",
|
||||
@@ -97,12 +244,47 @@
|
||||
"file": "contracts/example/Airdrop.sol",
|
||||
"line": 81
|
||||
},
|
||||
{
|
||||
"name": "RegisteredNullifier",
|
||||
"signature": "RegisteredNullifier()",
|
||||
"selector": "0x22cbc6a2",
|
||||
"file": "contracts/tests/TestAirdrop.sol",
|
||||
"line": 43
|
||||
},
|
||||
{
|
||||
"name": "InvalidMonthRange",
|
||||
"signature": "InvalidMonthRange()",
|
||||
"selector": "0x25e62788",
|
||||
"file": "contracts/libraries/Formatter.sol",
|
||||
"line": 11
|
||||
"line": 13
|
||||
},
|
||||
{
|
||||
"name": "ONLY_TEE_CAN_ACCESS",
|
||||
"signature": "ONLY_TEE_CAN_ACCESS()",
|
||||
"selector": "0x2822d0cb",
|
||||
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
|
||||
"line": 205
|
||||
},
|
||||
{
|
||||
"name": "ONLY_TEE_CAN_ACCESS",
|
||||
"signature": "ONLY_TEE_CAN_ACCESS()",
|
||||
"selector": "0x2822d0cb",
|
||||
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
|
||||
"line": 211
|
||||
},
|
||||
{
|
||||
"name": "ONLY_TEE_CAN_ACCESS",
|
||||
"signature": "ONLY_TEE_CAN_ACCESS()",
|
||||
"selector": "0x2822d0cb",
|
||||
"file": "contracts/registry/IdentityRegistryImplV1.sol",
|
||||
"line": 221
|
||||
},
|
||||
{
|
||||
"name": "ONLY_TEE_CAN_ACCESS",
|
||||
"signature": "ONLY_TEE_CAN_ACCESS()",
|
||||
"selector": "0x2822d0cb",
|
||||
"file": "contracts/registry/IdentityRegistryKycImplV1.sol",
|
||||
"line": 187
|
||||
},
|
||||
{
|
||||
"name": "UserIdentifierAlreadyRegistered",
|
||||
@@ -111,19 +293,61 @@
|
||||
"file": "contracts/example/Airdrop.sol",
|
||||
"line": 78
|
||||
},
|
||||
{
|
||||
"name": "UserIdentifierAlreadyRegistered",
|
||||
"signature": "UserIdentifierAlreadyRegistered()",
|
||||
"selector": "0x29393238",
|
||||
"file": "contracts/tests/TestAirdrop.sol",
|
||||
"line": 42
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsHash",
|
||||
"signature": "InvalidRootsHash()",
|
||||
"selector": "0x372c4a4b",
|
||||
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
|
||||
"line": 199
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsHash",
|
||||
"signature": "InvalidRootsHash()",
|
||||
"selector": "0x372c4a4b",
|
||||
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
|
||||
"line": 205
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsHash",
|
||||
"signature": "InvalidRootsHash()",
|
||||
"selector": "0x372c4a4b",
|
||||
"file": "contracts/registry/IdentityRegistryImplV1.sol",
|
||||
"line": 215
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsHash",
|
||||
"signature": "InvalidRootsHash()",
|
||||
"selector": "0x372c4a4b",
|
||||
"file": "contracts/registry/IdentityRegistryKycImplV1.sol",
|
||||
"line": 201
|
||||
},
|
||||
{
|
||||
"name": "InvalidFieldElement",
|
||||
"signature": "InvalidFieldElement()",
|
||||
"selector": "0x3ae4ed6b",
|
||||
"file": "contracts/libraries/Formatter.sol",
|
||||
"line": 13
|
||||
"line": 15
|
||||
},
|
||||
{
|
||||
"name": "InvalidPubkey",
|
||||
"signature": "InvalidPubkey()",
|
||||
"selector": "0x422cc3b7",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 199
|
||||
"line": 211
|
||||
},
|
||||
{
|
||||
"name": "InvalidPubkey",
|
||||
"signature": "InvalidPubkey()",
|
||||
"selector": "0x422cc3b7",
|
||||
"file": "contracts/libraries/RegisterProofVerifierLib.sol",
|
||||
"line": 35
|
||||
},
|
||||
{
|
||||
"name": "InvalidOlderThan",
|
||||
@@ -137,42 +361,63 @@
|
||||
"signature": "InvalidDscCommitmentRoot()",
|
||||
"selector": "0x4cb305bb",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 163
|
||||
"line": 175
|
||||
},
|
||||
{
|
||||
"name": "InvalidDscCommitmentRoot",
|
||||
"signature": "InvalidDscCommitmentRoot()",
|
||||
"selector": "0x4cb305bb",
|
||||
"file": "contracts/libraries/RegisterProofVerifierLib.sol",
|
||||
"line": 29
|
||||
},
|
||||
{
|
||||
"name": "HUB_NOT_SET",
|
||||
"signature": "HUB_NOT_SET()",
|
||||
"selector": "0x4ffa9998",
|
||||
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
|
||||
"line": 130
|
||||
"line": 183
|
||||
},
|
||||
{
|
||||
"name": "HUB_NOT_SET",
|
||||
"signature": "HUB_NOT_SET()",
|
||||
"selector": "0x4ffa9998",
|
||||
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
|
||||
"line": 138
|
||||
"line": 191
|
||||
},
|
||||
{
|
||||
"name": "HUB_NOT_SET",
|
||||
"signature": "HUB_NOT_SET()",
|
||||
"selector": "0x4ffa9998",
|
||||
"file": "contracts/registry/IdentityRegistryImplV1.sol",
|
||||
"line": 145
|
||||
"line": 201
|
||||
},
|
||||
{
|
||||
"name": "HUB_NOT_SET",
|
||||
"signature": "HUB_NOT_SET()",
|
||||
"selector": "0x4ffa9998",
|
||||
"file": "contracts/registry/IdentityRegistryKycImplV1.sol",
|
||||
"line": 181
|
||||
},
|
||||
{
|
||||
"name": "INVALID_COMMITMENT_ROOT",
|
||||
"signature": "INVALID_COMMITMENT_ROOT()",
|
||||
"selector": "0x52906601",
|
||||
"file": "contracts/IdentityVerificationHubImplV1.sol",
|
||||
"line": 162
|
||||
"line": 172
|
||||
},
|
||||
{
|
||||
"name": "UnauthorizedCaller",
|
||||
"signature": "UnauthorizedCaller()",
|
||||
"selector": "0x5c427cd9",
|
||||
"file": "contracts/abstract/SelfVerificationRoot.sol",
|
||||
"line": 46
|
||||
"line": 49
|
||||
},
|
||||
{
|
||||
"name": "UnauthorizedCaller",
|
||||
"signature": "UnauthorizedCaller()",
|
||||
"selector": "0x5c427cd9",
|
||||
"file": "contracts/abstract/SelfVerificationRootUpgradeable.sol",
|
||||
"line": 69
|
||||
},
|
||||
{
|
||||
"name": "UserIdentifierAlreadyMinted",
|
||||
@@ -200,7 +445,7 @@
|
||||
"signature": "CrossChainIsNotSupportedYet()",
|
||||
"selector": "0x61296fbb",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 179
|
||||
"line": 191
|
||||
},
|
||||
{
|
||||
"name": "AlreadyClaimed",
|
||||
@@ -216,19 +461,33 @@
|
||||
"file": "contracts/example/HappyBirthday.sol",
|
||||
"line": 67
|
||||
},
|
||||
{
|
||||
"name": "AlreadyClaimed",
|
||||
"signature": "AlreadyClaimed()",
|
||||
"selector": "0x646cf558",
|
||||
"file": "contracts/tests/TestAirdrop.sol",
|
||||
"line": 36
|
||||
},
|
||||
{
|
||||
"name": "InputTooShort",
|
||||
"signature": "InputTooShort()",
|
||||
"selector": "0x65ec0cf1",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 183
|
||||
"line": 195
|
||||
},
|
||||
{
|
||||
"name": "InvalidRegisterProof",
|
||||
"signature": "InvalidRegisterProof()",
|
||||
"selector": "0x67b61dc7",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 147
|
||||
"line": 159
|
||||
},
|
||||
{
|
||||
"name": "InvalidRegisterProof",
|
||||
"signature": "InvalidRegisterProof()",
|
||||
"selector": "0x67b61dc7",
|
||||
"file": "contracts/libraries/RegisterProofVerifierLib.sol",
|
||||
"line": 26
|
||||
},
|
||||
{
|
||||
"name": "RegistrationNotClosed",
|
||||
@@ -237,12 +496,19 @@
|
||||
"file": "contracts/example/Airdrop.sol",
|
||||
"line": 69
|
||||
},
|
||||
{
|
||||
"name": "RegistrationNotClosed",
|
||||
"signature": "RegistrationNotClosed()",
|
||||
"selector": "0x697e379b",
|
||||
"file": "contracts/tests/TestAirdrop.sol",
|
||||
"line": 39
|
||||
},
|
||||
{
|
||||
"name": "INVALID_DSC_PROOF",
|
||||
"signature": "INVALID_DSC_PROOF()",
|
||||
"selector": "0x6a86dd76",
|
||||
"file": "contracts/IdentityVerificationHubImplV1.sol",
|
||||
"line": 154
|
||||
"line": 164
|
||||
},
|
||||
{
|
||||
"name": "ClaimNotOpen",
|
||||
@@ -251,19 +517,89 @@
|
||||
"file": "contracts/example/Airdrop.sol",
|
||||
"line": 72
|
||||
},
|
||||
{
|
||||
"name": "ClaimNotOpen",
|
||||
"signature": "ClaimNotOpen()",
|
||||
"selector": "0x6b687806",
|
||||
"file": "contracts/tests/TestAirdrop.sol",
|
||||
"line": 40
|
||||
},
|
||||
{
|
||||
"name": "InvalidUidaiTimestamp",
|
||||
"signature": "InvalidUidaiTimestamp(uint256,uint256)",
|
||||
"selector": "0x6f26ab8d",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 203
|
||||
"line": 215
|
||||
},
|
||||
{
|
||||
"name": "InvalidUidaiTimestamp",
|
||||
"signature": "InvalidUidaiTimestamp(uint256,uint256)",
|
||||
"selector": "0x6f26ab8d",
|
||||
"file": "contracts/libraries/RegisterProofVerifierLib.sol",
|
||||
"line": 38
|
||||
},
|
||||
{
|
||||
"name": "INVALID_PROOF",
|
||||
"signature": "INVALID_PROOF()",
|
||||
"selector": "0x712eb087",
|
||||
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
|
||||
"line": 191
|
||||
},
|
||||
{
|
||||
"name": "INVALID_PROOF",
|
||||
"signature": "INVALID_PROOF()",
|
||||
"selector": "0x712eb087",
|
||||
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
|
||||
"line": 197
|
||||
},
|
||||
{
|
||||
"name": "INVALID_PROOF",
|
||||
"signature": "INVALID_PROOF()",
|
||||
"selector": "0x712eb087",
|
||||
"file": "contracts/registry/IdentityRegistryImplV1.sol",
|
||||
"line": 207
|
||||
},
|
||||
{
|
||||
"name": "INVALID_PROOF",
|
||||
"signature": "INVALID_PROOF()",
|
||||
"selector": "0x712eb087",
|
||||
"file": "contracts/registry/IdentityRegistryKycImplV1.sol",
|
||||
"line": 193
|
||||
},
|
||||
{
|
||||
"name": "INVALID_OFAC",
|
||||
"signature": "INVALID_OFAC()",
|
||||
"selector": "0x71b125ed",
|
||||
"file": "contracts/IdentityVerificationHubImplV1.sol",
|
||||
"line": 146
|
||||
"line": 156
|
||||
},
|
||||
{
|
||||
"name": "INVALID_IMAGE",
|
||||
"signature": "INVALID_IMAGE()",
|
||||
"selector": "0x7f91b413",
|
||||
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
|
||||
"line": 195
|
||||
},
|
||||
{
|
||||
"name": "INVALID_IMAGE",
|
||||
"signature": "INVALID_IMAGE()",
|
||||
"selector": "0x7f91b413",
|
||||
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
|
||||
"line": 201
|
||||
},
|
||||
{
|
||||
"name": "INVALID_IMAGE",
|
||||
"signature": "INVALID_IMAGE()",
|
||||
"selector": "0x7f91b413",
|
||||
"file": "contracts/registry/IdentityRegistryImplV1.sol",
|
||||
"line": 211
|
||||
},
|
||||
{
|
||||
"name": "INVALID_IMAGE",
|
||||
"signature": "INVALID_IMAGE()",
|
||||
"selector": "0x7f91b413",
|
||||
"file": "contracts/registry/IdentityRegistryKycImplV1.sol",
|
||||
"line": 197
|
||||
},
|
||||
{
|
||||
"name": "InvalidForbiddenCountries",
|
||||
@@ -284,7 +620,7 @@
|
||||
"signature": "InsufficientCharcodeLen()",
|
||||
"selector": "0x86d41225",
|
||||
"file": "contracts/libraries/CircuitAttributeHandlerV2.sol",
|
||||
"line": 16
|
||||
"line": 17
|
||||
},
|
||||
{
|
||||
"name": "InsufficientCharcodeLen",
|
||||
@@ -298,42 +634,49 @@
|
||||
"signature": "InvalidDayRange()",
|
||||
"selector": "0x8930acef",
|
||||
"file": "contracts/libraries/Formatter.sol",
|
||||
"line": 12
|
||||
"line": 14
|
||||
},
|
||||
{
|
||||
"name": "LENGTH_MISMATCH",
|
||||
"signature": "LENGTH_MISMATCH()",
|
||||
"selector": "0x899ef10d",
|
||||
"file": "contracts/IdentityVerificationHubImplV1.sol",
|
||||
"line": 126
|
||||
"line": 136
|
||||
},
|
||||
{
|
||||
"name": "NO_VERIFIER_SET",
|
||||
"signature": "NO_VERIFIER_SET()",
|
||||
"selector": "0x8e727f46",
|
||||
"file": "contracts/IdentityVerificationHubImplV1.sol",
|
||||
"line": 130
|
||||
"line": 140
|
||||
},
|
||||
{
|
||||
"name": "InvalidCscaRoot",
|
||||
"signature": "InvalidCscaRoot()",
|
||||
"selector": "0x8f1b44c7",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 167
|
||||
"line": 179
|
||||
},
|
||||
{
|
||||
"name": "InvalidCscaRoot",
|
||||
"signature": "InvalidCscaRoot()",
|
||||
"selector": "0x8f1b44c7",
|
||||
"file": "contracts/libraries/DscProofVerifierLib.sol",
|
||||
"line": 23
|
||||
},
|
||||
{
|
||||
"name": "INVALID_REGISTER_PROOF",
|
||||
"signature": "INVALID_REGISTER_PROOF()",
|
||||
"selector": "0x9003ac4d",
|
||||
"file": "contracts/IdentityVerificationHubImplV1.sol",
|
||||
"line": 150
|
||||
"line": 160
|
||||
},
|
||||
{
|
||||
"name": "UserContextDataTooShort",
|
||||
"signature": "UserContextDataTooShort()",
|
||||
"selector": "0x94ec3503",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 187
|
||||
"line": 199
|
||||
},
|
||||
{
|
||||
"name": "NotWithinBirthdayWindow",
|
||||
@@ -347,56 +690,70 @@
|
||||
"signature": "INVALID_CSCA_ROOT()",
|
||||
"selector": "0xa294ad3c",
|
||||
"file": "contracts/IdentityVerificationHubImplV1.sol",
|
||||
"line": 170
|
||||
"line": 180
|
||||
},
|
||||
{
|
||||
"name": "InvalidDataFormat",
|
||||
"signature": "InvalidDataFormat()",
|
||||
"selector": "0xa512e2ff",
|
||||
"file": "contracts/abstract/SelfVerificationRoot.sol",
|
||||
"line": 42
|
||||
"line": 45
|
||||
},
|
||||
{
|
||||
"name": "InvalidDataFormat",
|
||||
"signature": "InvalidDataFormat()",
|
||||
"selector": "0xa512e2ff",
|
||||
"file": "contracts/abstract/SelfVerificationRootUpgradeable.sol",
|
||||
"line": 65
|
||||
},
|
||||
{
|
||||
"name": "ConfigNotSet",
|
||||
"signature": "ConfigNotSet()",
|
||||
"selector": "0xace124bc",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 195
|
||||
"line": 207
|
||||
},
|
||||
{
|
||||
"name": "InvalidDateLength",
|
||||
"signature": "InvalidDateLength()",
|
||||
"selector": "0xb3375953",
|
||||
"file": "contracts/libraries/Formatter.sol",
|
||||
"line": 9
|
||||
"line": 11
|
||||
},
|
||||
{
|
||||
"name": "ONLY_HUB_CAN_ACCESS",
|
||||
"signature": "ONLY_HUB_CAN_ACCESS()",
|
||||
"selector": "0xba0318cb",
|
||||
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
|
||||
"line": 132
|
||||
"line": 185
|
||||
},
|
||||
{
|
||||
"name": "ONLY_HUB_CAN_ACCESS",
|
||||
"signature": "ONLY_HUB_CAN_ACCESS()",
|
||||
"selector": "0xba0318cb",
|
||||
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
|
||||
"line": 140
|
||||
"line": 193
|
||||
},
|
||||
{
|
||||
"name": "ONLY_HUB_CAN_ACCESS",
|
||||
"signature": "ONLY_HUB_CAN_ACCESS()",
|
||||
"selector": "0xba0318cb",
|
||||
"file": "contracts/registry/IdentityRegistryImplV1.sol",
|
||||
"line": 147
|
||||
"line": 203
|
||||
},
|
||||
{
|
||||
"name": "ONLY_HUB_CAN_ACCESS",
|
||||
"signature": "ONLY_HUB_CAN_ACCESS()",
|
||||
"selector": "0xba0318cb",
|
||||
"file": "contracts/registry/IdentityRegistryKycImplV1.sol",
|
||||
"line": 183
|
||||
},
|
||||
{
|
||||
"name": "INVALID_FORBIDDEN_COUNTRIES",
|
||||
"signature": "INVALID_FORBIDDEN_COUNTRIES()",
|
||||
"selector": "0xbf21b11c",
|
||||
"file": "contracts/IdentityVerificationHubImplV1.sol",
|
||||
"line": 142
|
||||
"line": 152
|
||||
},
|
||||
{
|
||||
"name": "NotRegistered",
|
||||
@@ -405,68 +762,138 @@
|
||||
"file": "contracts/example/Airdrop.sol",
|
||||
"line": 63
|
||||
},
|
||||
{
|
||||
"name": "NotRegistered",
|
||||
"signature": "NotRegistered(address)",
|
||||
"selector": "0xbfc6c337",
|
||||
"file": "contracts/tests/TestAirdrop.sol",
|
||||
"line": 37
|
||||
},
|
||||
{
|
||||
"name": "InvalidOfacRoots",
|
||||
"signature": "InvalidOfacRoots()",
|
||||
"selector": "0xc67a44d2",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 211
|
||||
"line": 223
|
||||
},
|
||||
{
|
||||
"name": "InvalidOfacRoots",
|
||||
"signature": "InvalidOfacRoots()",
|
||||
"selector": "0xc67a44d2",
|
||||
"file": "contracts/libraries/OfacCheckLib.sol",
|
||||
"line": 19
|
||||
},
|
||||
{
|
||||
"name": "CurrentDateNotInValidRange",
|
||||
"signature": "CurrentDateNotInValidRange()",
|
||||
"selector": "0xcf46551c",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 143
|
||||
"line": 155
|
||||
},
|
||||
{
|
||||
"name": "INVALID_VC_AND_DISCLOSE_PROOF",
|
||||
"signature": "INVALID_VC_AND_DISCLOSE_PROOF()",
|
||||
"selector": "0xd4d37a7a",
|
||||
"file": "contracts/IdentityVerificationHubImplV1.sol",
|
||||
"line": 158
|
||||
"line": 168
|
||||
},
|
||||
{
|
||||
"name": "AttestationIdMismatch",
|
||||
"signature": "AttestationIdMismatch()",
|
||||
"selector": "0xd7ca437d",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 207
|
||||
"line": 219
|
||||
},
|
||||
{
|
||||
"name": "InvalidVcAndDiscloseProof",
|
||||
"signature": "InvalidVcAndDiscloseProof()",
|
||||
"selector": "0xda7bd3a6",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 155
|
||||
"line": 167
|
||||
},
|
||||
{
|
||||
"name": "InvalidVcAndDiscloseProof",
|
||||
"signature": "InvalidVcAndDiscloseProof()",
|
||||
"selector": "0xda7bd3a6",
|
||||
"file": "contracts/libraries/ProofVerifierLib.sol",
|
||||
"line": 18
|
||||
},
|
||||
{
|
||||
"name": "RegistryNotSet",
|
||||
"signature": "RegistryNotSet()",
|
||||
"selector": "0xe048e710",
|
||||
"file": "contracts/libraries/RootCheckLib.sol",
|
||||
"line": 25
|
||||
},
|
||||
{
|
||||
"name": "INVALID_REVEALED_DATA_TYPE",
|
||||
"signature": "INVALID_REVEALED_DATA_TYPE()",
|
||||
"selector": "0xe0f15544",
|
||||
"file": "contracts/IdentityVerificationHubImplV1.sol",
|
||||
"line": 174
|
||||
"line": 184
|
||||
},
|
||||
{
|
||||
"name": "ScopeMismatch",
|
||||
"signature": "ScopeMismatch()",
|
||||
"selector": "0xe7bee380",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 175
|
||||
"line": 187
|
||||
},
|
||||
{
|
||||
"name": "InvalidUserIdentifierInProof",
|
||||
"signature": "InvalidUserIdentifierInProof()",
|
||||
"selector": "0xebbcc178",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 191
|
||||
"line": 203
|
||||
},
|
||||
{
|
||||
"name": "InvalidPubkeyCommitment",
|
||||
"signature": "InvalidPubkeyCommitment()",
|
||||
"selector": "0xebc2fedc",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 227
|
||||
},
|
||||
{
|
||||
"name": "InvalidPubkeyCommitment",
|
||||
"signature": "InvalidPubkeyCommitment()",
|
||||
"selector": "0xebc2fedc",
|
||||
"file": "contracts/libraries/RegisterProofVerifierLib.sol",
|
||||
"line": 41
|
||||
},
|
||||
{
|
||||
"name": "CURRENT_DATE_NOT_IN_VALID_RANGE",
|
||||
"signature": "CURRENT_DATE_NOT_IN_VALID_RANGE()",
|
||||
"selector": "0xed8cf9ff",
|
||||
"file": "contracts/IdentityVerificationHubImplV1.sol",
|
||||
"line": 134
|
||||
"line": 144
|
||||
},
|
||||
{
|
||||
"name": "INVALID_ROOT_CA",
|
||||
"signature": "INVALID_ROOT_CA()",
|
||||
"selector": "0xee57533e",
|
||||
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
|
||||
"line": 193
|
||||
},
|
||||
{
|
||||
"name": "INVALID_ROOT_CA",
|
||||
"signature": "INVALID_ROOT_CA()",
|
||||
"selector": "0xee57533e",
|
||||
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
|
||||
"line": 199
|
||||
},
|
||||
{
|
||||
"name": "INVALID_ROOT_CA",
|
||||
"signature": "INVALID_ROOT_CA()",
|
||||
"selector": "0xee57533e",
|
||||
"file": "contracts/registry/IdentityRegistryImplV1.sol",
|
||||
"line": 209
|
||||
},
|
||||
{
|
||||
"name": "INVALID_ROOT_CA",
|
||||
"signature": "INVALID_ROOT_CA()",
|
||||
"selector": "0xee57533e",
|
||||
"file": "contracts/registry/IdentityRegistryKycImplV1.sol",
|
||||
"line": 195
|
||||
},
|
||||
{
|
||||
"name": "InvalidUserIdentifier",
|
||||
@@ -489,25 +916,67 @@
|
||||
"file": "contracts/example/SelfPassportERC721.sol",
|
||||
"line": 49
|
||||
},
|
||||
{
|
||||
"name": "InvalidUserIdentifier",
|
||||
"signature": "InvalidUserIdentifier()",
|
||||
"selector": "0xf0c426db",
|
||||
"file": "contracts/tests/TestAirdrop.sol",
|
||||
"line": 41
|
||||
},
|
||||
{
|
||||
"name": "INVALID_OLDER_THAN",
|
||||
"signature": "INVALID_OLDER_THAN()",
|
||||
"selector": "0xf0e539b9",
|
||||
"file": "contracts/IdentityVerificationHubImplV1.sol",
|
||||
"line": 138
|
||||
"line": 148
|
||||
},
|
||||
{
|
||||
"name": "InvalidIdentityCommitmentRoot",
|
||||
"signature": "InvalidIdentityCommitmentRoot()",
|
||||
"selector": "0xf53393a7",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 159
|
||||
"line": 171
|
||||
},
|
||||
{
|
||||
"name": "InvalidIdentityCommitmentRoot",
|
||||
"signature": "InvalidIdentityCommitmentRoot()",
|
||||
"selector": "0xf53393a7",
|
||||
"file": "contracts/libraries/RootCheckLib.sol",
|
||||
"line": 19
|
||||
},
|
||||
{
|
||||
"name": "TEE_NOT_SET",
|
||||
"signature": "TEE_NOT_SET()",
|
||||
"selector": "0xfc833fc6",
|
||||
"file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol",
|
||||
"line": 203
|
||||
},
|
||||
{
|
||||
"name": "TEE_NOT_SET",
|
||||
"signature": "TEE_NOT_SET()",
|
||||
"selector": "0xfc833fc6",
|
||||
"file": "contracts/registry/IdentityRegistryIdCardImplV1.sol",
|
||||
"line": 209
|
||||
},
|
||||
{
|
||||
"name": "TEE_NOT_SET",
|
||||
"signature": "TEE_NOT_SET()",
|
||||
"selector": "0xfc833fc6",
|
||||
"file": "contracts/registry/IdentityRegistryImplV1.sol",
|
||||
"line": 219
|
||||
},
|
||||
{
|
||||
"name": "TEE_NOT_SET",
|
||||
"signature": "TEE_NOT_SET()",
|
||||
"selector": "0xfc833fc6",
|
||||
"file": "contracts/registry/IdentityRegistryKycImplV1.sol",
|
||||
"line": 185
|
||||
},
|
||||
{
|
||||
"name": "LengthMismatch",
|
||||
"signature": "LengthMismatch()",
|
||||
"selector": "0xff633a38",
|
||||
"file": "contracts/IdentityVerificationHubImplV2.sol",
|
||||
"line": 135
|
||||
"line": 147
|
||||
}
|
||||
]
|
||||
|
||||
@@ -90,8 +90,8 @@
|
||||
"DeployIdCardRegistryModule#IdentityRegistryIdCardImplV1": "0xF4781c7e801D1E49aa3A95537FaEF7718f4499Cd",
|
||||
"DeployIdCardRegistryModule#IdentityRegistry": "0x6B39222c3b98003010695cE0A31C9b1a61e07DdC",
|
||||
"DeployAadhaarRegistryModule#PoseidonT3": "0xB80d454C2BF6c886EfA51Af830F43ac3147dCE15",
|
||||
"DeployAadhaarRegistryModule#IdentityRegistryAadhaarImplV1": "0x74A2848D945eCffeE325dAbc9E0b72c118fAD327",
|
||||
"DeployAadhaarRegistryModule#IdentityRegistry": "0x9cbB71468f93672DBF50f511c038eAF9fAB04732",
|
||||
"DeployAadhaarRegistryModule#IdentityRegistryAadhaarImplV1": "0x2b8b23d88e29534dfd61eb64d178b0e77681f760",
|
||||
"DeployAadhaarRegistryModule#IdentityRegistry": "0x52A18C19a35Ac6Ef1673B8Fa5b7BD01dBfA2D389",
|
||||
"DeployHubV2#CustomVerifier": "0x3C154D1Bb35589e82B13892dE5283ADAfaDC473f",
|
||||
"DeployHubV2#IdentityVerificationHubImplV2": "0xC49b7FD44Cb4bE0482AEa3335Eb2CeFb1b81B0C9",
|
||||
"DeployHubV2#IdentityVerificationHub": "0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74",
|
||||
@@ -100,10 +100,22 @@
|
||||
"DeployNewHubAndUpgradee#IdentityVerificationHubImplV2": "0x48985ec4f71cBC8f387c5C77143110018560c7eD",
|
||||
"DeployKycRegistryModule#PCR0Manager": "0xf2810D5E9938816D42F0Ae69D33F013a23C0aED2",
|
||||
"DeployKycRegistryModule#PoseidonT3": "0x163983BAe19dE94A007C6C502b7389F6C359C818",
|
||||
"DeployKycRegistryModule#Verifier_gcp_jwt": "0x13ee8CEa15a262D81a245b37889F7b4bEd015f4c",
|
||||
"DeployKycRegistryModule#Verifier_gcp_jwt": "0xAF73bE5cf1E826Df56292D9FD41D37CaBfb59344",
|
||||
"DeployKycRegistryModule#IdentityRegistryKycImplV1": "0x94f6DE38E10140B9E3963a770B5B769b38459a3B",
|
||||
"DeployKycRegistryModule#IdentityRegistry": "0x90e907E4AaB6e9bcFB94997Af4A097e8CAadBdf3",
|
||||
"UpdateAllRegistries#PCR0Manager": "0xf2810D5E9938816D42F0Ae69D33F013a23C0aED2",
|
||||
"UpdateAllRegistries#PCR0Manager": "0x9743fe2C1c3D2b068c56dE314e9B10DA9c904717",
|
||||
"DeployAllVerifiers#Verifier_vc_and_disclose_kyc": "0xAAFA189a079D04462e8ab596d9c103e081A1c810",
|
||||
"UpgradeKycRegistryModule#KycRegistryProxy": "0x90e907E4AaB6e9bcFB94997Af4A097e8CAadBdf3",
|
||||
"UpgradeKycRegistryModule#PoseidonT3": "0x0728806F5b99e6527847fF29F55A1ae4778D1498",
|
||||
"UpgradeKycRegistryModule#IdentityRegistryKycImplV1": "0x72cB0be300e222a02C2c7aF2bb289667c8Cf2bdd",
|
||||
"UpdateAllRegistries#a3": "0x90e907E4AaB6e9bcFB94997Af4A097e8CAadBdf3",
|
||||
"DeployAllVerifiers#Verifier_vc_and_disclose_kyc": "0xAAFA189a079D04462e8ab596d9c103e081A1c810"
|
||||
"UpgradeAadhaarRegistryModule#AadhaarRegistryProxy": "0x52A18C19a35Ac6Ef1673B8Fa5b7BD01dBfA2D389",
|
||||
"UpgradeAadhaarRegistryModule#PoseidonT3": "0x424Ada67de0DBEFC1Bb6dEfeACF64E3Ae81B2411",
|
||||
"UpgradeAadhaarRegistryModule#IdentityRegistryAadhaarImplV1": "0xf66fdaC87159d0362b1E26CD7739C92f4AB4f9b4",
|
||||
"UpgradeIdCardRegistryModule#IdCardRegistryProxy": "0x6B39222c3b98003010695cE0A31C9b1a61e07DdC",
|
||||
"UpgradeIdCardRegistryModule#PoseidonT3": "0xFd6B7b646B91298ca106c390F9f1b61D92C694F7",
|
||||
"UpgradeIdCardRegistryModule#IdentityRegistryIdCardImplV1": "0x620358cb15773400F4d84217606172540f425156",
|
||||
"UpgradeRegistryModule#RegistryProxy": "0x1651ec77c3dC5997eC05f3EE6C2B0b904b516d1d",
|
||||
"UpgradeRegistryModule#PoseidonT3": "0x873B8F582A6EE01c4741349c99f69051B8eF1315",
|
||||
"UpgradeRegistryModule#IdentityRegistryImplV1": "0x663D0C2548F14faF440FdF8118FCc811657B5A9C"
|
||||
}
|
||||
|
||||
@@ -99,6 +99,14 @@
|
||||
"DeployAllVerifiers#Verifier_register_kyc": "0xbc15010D9748A5e7c0B947D0c0aCb31bD57a0626",
|
||||
"DeployAllVerifiers#Verifier_vc_and_disclose_kyc": "0xdB0454156bBa5e5b9CA97be350eCc178ddE20b0f",
|
||||
"UpgradeKycRegistryModule#KycRegistryProxy": "0x238f83c641020Ef9636694A846019FD2a105C4f0",
|
||||
"UpgradeKycRegistryModule#PoseidonT3": "0xAD1b73963c7386bF83af187De03136a58111A637",
|
||||
"UpgradeKycRegistryModule#IdentityRegistryKycImplV1": "0x71b820E2D71a9dba77dC00291873cba257ac66be"
|
||||
"UpgradeKycRegistryModule#PoseidonT3": "0x7460578F2012e8a97e56F21D1D9EDd8DC5DcCB10",
|
||||
"UpgradeKycRegistryModule#IdentityRegistryKycImplV1": "0xa3140DbC877A0Ac5CD8d0Eb9DB9930d62bF28c2c",
|
||||
"UpgradeAadhaarRegistryModule#PoseidonT3": "0x21B81C63A66B08BfcAf53Cb2D66e0Ae8886D9ACd",
|
||||
"UpgradeAadhaarRegistryModule#IdentityRegistryAadhaarImplV1": "0x06bB9e39bc89fa98cD3523e4f33d2f62d6Cb4991",
|
||||
"DeployRegistryModule#IdentityRegistry": "0x37F5CB8cB1f6B00aa768D8aA99F1A9289802A968",
|
||||
"DeployIdCardRegistryModule#IdentityRegistry": "0xeAD1E6Ec29c1f3D33a0662f253a3a94D189566E1",
|
||||
"UpgradeRegistryModule#PoseidonT3": "0xA02587616062cD1808BAbB8840376420ED5e56eB",
|
||||
"UpgradeRegistryModule#IdentityRegistryImplV1": "0x9bfBCe58f02e7c4501d51c6b1FB41b1C1ccc2e6C",
|
||||
"UpgradeIdCardRegistryModule#PoseidonT3": "0x3113773f77981e198161C8e561d32Ca99AA42C6D",
|
||||
"UpgradeIdCardRegistryModule#IdentityRegistryIdCardImplV1": "0xD192CD1780Dd0ceCedd626303d9F28b40949eaa3"
|
||||
}
|
||||
|
||||
@@ -1,51 +1,42 @@
|
||||
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
|
||||
import { artifacts } from "hardhat";
|
||||
import { ethers } from "ethers";
|
||||
// import { artifacts } from "hardhat";
|
||||
// import { ethers } from "ethers";
|
||||
|
||||
export default buildModule("DeployKycRegistryModule", (m) => {
|
||||
// Deploy PoseidonT3
|
||||
console.log("📚 Deploying PoseidonT3 library...");
|
||||
const poseidonT3 = m.library("PoseidonT3");
|
||||
// // Deploy PoseidonT3
|
||||
// console.log("📚 Deploying PoseidonT3 library...");
|
||||
// const poseidonT3 = m.library("PoseidonT3");
|
||||
|
||||
console.log("🏗️ Deploying IdentityRegistryKycImplV1 implementation...");
|
||||
// Deploy IdentityRegistryImplV1
|
||||
const identityRegistryKycImpl = m.contract("IdentityRegistryKycImplV1", [], {
|
||||
libraries: { PoseidonT3: poseidonT3 },
|
||||
});
|
||||
// console.log("🏗️ Deploying IdentityRegistryKycImplV1 implementation...");
|
||||
// // Deploy IdentityRegistryImplV1
|
||||
// const identityRegistryKycImpl = m.contract("IdentityRegistryKycImplV1", [], {
|
||||
// libraries: { PoseidonT3: poseidonT3 },
|
||||
// });
|
||||
|
||||
console.log("⚙️ Preparing registry initialization data...");
|
||||
// Get the interface and encode the initialize function call
|
||||
const registryInterface = getRegistryInitializeData();
|
||||
// console.log("⚙️ Preparing registry initialization data...");
|
||||
// // Get the interface and encode the initialize function call
|
||||
// const registryInterface = getRegistryInitializeData();
|
||||
|
||||
const registryInitData = registryInterface.encodeFunctionData("initialize", [ethers.ZeroAddress, ethers.ZeroAddress]);
|
||||
console.log(" Init data:", registryInitData);
|
||||
// const registryInitData = registryInterface.encodeFunctionData("initialize", [ethers.ZeroAddress, ethers.ZeroAddress]);
|
||||
// console.log(" Init data:", registryInitData);
|
||||
|
||||
console.log("🚀 Deploying IdentityRegistry proxy...");
|
||||
// Deploy the proxy contract with the implementation address and initialization data
|
||||
const registry = m.contract("IdentityRegistry", [identityRegistryKycImpl, registryInitData]);
|
||||
// console.log("🚀 Deploying IdentityRegistry proxy...");
|
||||
// // Deploy the proxy contract with the implementation address and initialization data
|
||||
// const registry = m.contract("IdentityRegistry", [identityRegistryKycImpl, registryInitData]);
|
||||
|
||||
// Redeploy verifier — circuit changed due to new trusted setup contributions
|
||||
const gcpKycVerifier = m.contract("Verifier_gcp_jwt", []);
|
||||
|
||||
// PCR0Manager not deployed - using existing mainnet PCR0Manager at 0x9743fe2C1c3D2b068c56dE314e9B10DA9c904717
|
||||
// const pcr0Manager = m.contract("PCR0Manager", []);
|
||||
|
||||
console.log("✅ Registry deployment module setup complete!");
|
||||
console.log(" 📋 Summary:");
|
||||
console.log(" - PoseidonT3: Library");
|
||||
console.log(" - IdentityRegistryKycImplV1: Implementation contract");
|
||||
console.log(" - IdentityRegistry: Proxy contract");
|
||||
console.log(" - Verifier_gcp_jwt: GCP JWT verifier contract");
|
||||
|
||||
return {
|
||||
poseidonT3,
|
||||
identityRegistryKycImpl,
|
||||
registry,
|
||||
gcpKycVerifier,
|
||||
};
|
||||
});
|
||||
|
||||
function getRegistryInitializeData() {
|
||||
const registryArtifact = artifacts.readArtifactSync("IdentityRegistryKycImplV1");
|
||||
const registryInterface = new ethers.Interface(registryArtifact.abi);
|
||||
return registryInterface;
|
||||
}
|
||||
// function getRegistryInitializeData() {
|
||||
// const registryArtifact = artifacts.readArtifactSync("IdentityRegistryKycImplV1");
|
||||
// const registryInterface = new ethers.Interface(registryArtifact.abi);
|
||||
// return registryInterface;
|
||||
// }
|
||||
|
||||
@@ -34,13 +34,14 @@ const registries = {
|
||||
// },
|
||||
"DeployKycRegistryModule#IdentityRegistry": {
|
||||
shouldChange: true,
|
||||
hub: "0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF",
|
||||
nameAndDobOfac: "12056959379782485690824392224737824782985009863971097094085968061978428696483",
|
||||
nameAndYobOfac: "14482015433179009576094845155298164108788397224633034095648782513909282765564",
|
||||
onlyTEEAddress: "0xe6b2856a51a17bd4edeb88b3f74370d64475b0fc",
|
||||
gcpJWTVerifier: "0x87785cC7E9Bc70f87E6F454235214bDEc853C044",
|
||||
pcr0Manager: "0x9743fe2C1c3D2b068c56dE314e9B10DA9c904717",
|
||||
gcpRootCAPubkeyHash: "14165687497759817957828709957846495993787741657460065475757428560999622217191",
|
||||
// hub: "0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF",
|
||||
// nameAndDobOfac: "12056959379782485690824392224737824782985009863971097094085968061978428696483",
|
||||
// nameAndYobOfac: "14482015433179009576094845155298164108788397224633034095648782513909282765564",
|
||||
// onlyTEEAddress: "0xe6b2856a51a17bd4edeb88b3f74370d64475b0fc",
|
||||
// gcpJWTVerifier: "0xAF73bE5cf1E826Df56292D9FD41D37CaBfb59344",
|
||||
imageDigest: "0x2a18971dae026c46b74661fbbb4a88a0da96899db78aca7b138e52ebda396f74",
|
||||
pcr0Manager: "0xf2810D5E9938816D42F0Ae69D33F013a23C0aED2",
|
||||
// gcpRootCAPubkeyHash: "14165687497759817957828709957846495993787741657460065475757428560999622217191",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
36
contracts/ignition/modules/upgrade/upgradeAadhaarRegistry.ts
Normal file
36
contracts/ignition/modules/upgrade/upgradeAadhaarRegistry.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
|
||||
import hre from "hardhat";
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default buildModule("UpgradeAadhaarRegistryModule", (m) => {
|
||||
const networkName = hre.network.config.chainId;
|
||||
|
||||
const deployedAddressesPath = path.join(__dirname, `../../deployments/chain-${networkName}/deployed_addresses.json`);
|
||||
const deployedAddresses = JSON.parse(readFileSync(deployedAddressesPath, "utf8"));
|
||||
|
||||
const aadhaarProxyAddress = deployedAddresses["DeployAadhaarRegistryModule#IdentityRegistry"];
|
||||
if (!aadhaarProxyAddress) {
|
||||
throw new Error("Aadhaar Registry proxy address not found in deployed_addresses.json");
|
||||
}
|
||||
|
||||
// Deploy PoseidonT3 library (required by Aadhaar registry)
|
||||
const poseidonT3 = m.library("PoseidonT3");
|
||||
|
||||
// Deploy new Aadhaar implementation with PoseidonT3 linked
|
||||
const newAadhaarImpl = m.contract("IdentityRegistryAadhaarImplV1", [], {
|
||||
libraries: { PoseidonT3: poseidonT3 },
|
||||
});
|
||||
|
||||
const aadhaarProxy = m.contractAt("IdentityRegistryAadhaarImplV1", aadhaarProxyAddress, {
|
||||
id: "AadhaarRegistryProxy",
|
||||
});
|
||||
m.call(aadhaarProxy, "upgradeToAndCall", [newAadhaarImpl, "0x"], {
|
||||
after: [newAadhaarImpl],
|
||||
});
|
||||
|
||||
return {
|
||||
poseidonT3,
|
||||
newAadhaarImpl,
|
||||
};
|
||||
});
|
||||
36
contracts/ignition/modules/upgrade/upgradeIdCardRegistry.ts
Normal file
36
contracts/ignition/modules/upgrade/upgradeIdCardRegistry.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
|
||||
import hre from "hardhat";
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default buildModule("UpgradeIdCardRegistryModule", (m) => {
|
||||
const networkName = hre.network.config.chainId;
|
||||
|
||||
const deployedAddressesPath = path.join(__dirname, `../../deployments/chain-${networkName}/deployed_addresses.json`);
|
||||
const deployedAddresses = JSON.parse(readFileSync(deployedAddressesPath, "utf8"));
|
||||
|
||||
const idCardProxyAddress = deployedAddresses["DeployIdCardRegistryModule#IdentityRegistry"];
|
||||
if (!idCardProxyAddress) {
|
||||
throw new Error("IdCard Registry proxy address not found in deployed_addresses.json");
|
||||
}
|
||||
|
||||
// Deploy PoseidonT3 library (required by IdCard registry)
|
||||
const poseidonT3 = m.library("PoseidonT3");
|
||||
|
||||
// Deploy new IdCard implementation with PoseidonT3 linked
|
||||
const newIdCardImpl = m.contract("IdentityRegistryIdCardImplV1", [], {
|
||||
libraries: { PoseidonT3: poseidonT3 },
|
||||
});
|
||||
|
||||
const idCardProxy = m.contractAt("IdentityRegistryIdCardImplV1", idCardProxyAddress, {
|
||||
id: "IdCardRegistryProxy",
|
||||
});
|
||||
m.call(idCardProxy, "upgradeToAndCall", [newIdCardImpl, "0x"], {
|
||||
after: [newIdCardImpl],
|
||||
});
|
||||
|
||||
return {
|
||||
poseidonT3,
|
||||
newIdCardImpl,
|
||||
};
|
||||
});
|
||||
36
contracts/ignition/modules/upgrade/upgradeRegistry.ts
Normal file
36
contracts/ignition/modules/upgrade/upgradeRegistry.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
|
||||
import hre from "hardhat";
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default buildModule("UpgradeRegistryModule", (m) => {
|
||||
const networkName = hre.network.config.chainId;
|
||||
|
||||
const deployedAddressesPath = path.join(__dirname, `../../deployments/chain-${networkName}/deployed_addresses.json`);
|
||||
const deployedAddresses = JSON.parse(readFileSync(deployedAddressesPath, "utf8"));
|
||||
|
||||
const registryProxyAddress = deployedAddresses["DeployRegistryModule#IdentityRegistry"];
|
||||
if (!registryProxyAddress) {
|
||||
throw new Error("Passport Registry proxy address not found in deployed_addresses.json");
|
||||
}
|
||||
|
||||
// Deploy PoseidonT3 library (required by Passport registry)
|
||||
const poseidonT3 = m.library("PoseidonT3");
|
||||
|
||||
// Deploy new Passport implementation with PoseidonT3 linked
|
||||
const newRegistryImpl = m.contract("IdentityRegistryImplV1", [], {
|
||||
libraries: { PoseidonT3: poseidonT3 },
|
||||
});
|
||||
|
||||
const registryProxy = m.contractAt("IdentityRegistryImplV1", registryProxyAddress, {
|
||||
id: "RegistryProxy",
|
||||
});
|
||||
m.call(registryProxy, "upgradeToAndCall", [newRegistryImpl, "0x"], {
|
||||
after: [newRegistryImpl],
|
||||
});
|
||||
|
||||
return {
|
||||
poseidonT3,
|
||||
newRegistryImpl,
|
||||
};
|
||||
});
|
||||
@@ -16,9 +16,14 @@ The upgrade tooling provides:
|
||||
```bash
|
||||
# Single command to validate, deploy, and propose
|
||||
npx hardhat upgrade --contract IdentityVerificationHub --network celo --changelog "Added feature X"
|
||||
|
||||
# Dry run first to validate without deploying
|
||||
npx hardhat upgrade --contract IdentityRegistryKyc --network celo-sepolia --dry-run
|
||||
```
|
||||
|
||||
## The `upgrade` Command
|
||||
## Commands
|
||||
|
||||
### `upgrade`
|
||||
|
||||
Validates, deploys, and creates a Safe multisig proposal in one step.
|
||||
|
||||
@@ -27,35 +32,72 @@ npx hardhat upgrade \
|
||||
--contract <ContractId> \
|
||||
--network <network> \
|
||||
[--changelog <message>] \
|
||||
[--prepare-only]
|
||||
[--prepare-only] \
|
||||
[--dry-run] \
|
||||
[--skip-commit]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- `--contract` - Contract to upgrade (IdentityVerificationHub, IdentityRegistry, etc.)
|
||||
- `--network` - Target network (celo, sepolia, localhost)
|
||||
- `--changelog` - Description of changes
|
||||
- `--prepare-only` - Deploy implementation without creating Safe proposal
|
||||
| Flag | Description |
|
||||
| ---------------- | --------------------------------------------------------------------- |
|
||||
| `--contract` | Contract to upgrade (see [Supported Contracts](#supported-contracts)) |
|
||||
| `--network` | Target network: `celo`, `celo-sepolia` |
|
||||
| `--changelog` | Description of changes for the version history |
|
||||
| `--prepare-only` | Deploy implementation without creating Safe proposal |
|
||||
| `--dry-run` | Simulate the full flow without deploying or proposing |
|
||||
| `--skip-commit` | Skip automatic git commit after deployment |
|
||||
|
||||
**What it does:**
|
||||
|
||||
1. ✅ Validates `@custom:version` increment
|
||||
2. ✅ Checks `reinitializer(N)` matches expected version
|
||||
3. ✅ Validates storage layout compatibility
|
||||
4. ✅ Clears cache and compiles fresh
|
||||
5. ✅ Compares bytecode (warns if unchanged)
|
||||
6. ✅ Deploys new implementation
|
||||
7. ✅ Updates deployment registry
|
||||
8. ✅ Creates git commit and tag
|
||||
9. ✅ Creates Safe proposal (or outputs manual instructions)
|
||||
1. Validates `@custom:version` increment
|
||||
2. Checks `reinitializer(N)` matches expected version
|
||||
3. Validates storage layout compatibility
|
||||
4. Clears cache and compiles fresh
|
||||
5. Compares bytecode (warns if unchanged)
|
||||
6. Deploys new implementation (+ PoseidonT3 library for registry contracts)
|
||||
7. Verifies on block explorer
|
||||
8. Updates `deployments/registry.json`
|
||||
9. Creates git commit and tag
|
||||
10. Creates Safe proposal (or outputs manual instructions)
|
||||
|
||||
## Utility Commands
|
||||
### `upgrade:prepare`
|
||||
|
||||
Deploys the new implementation only, without creating a Safe proposal.
|
||||
|
||||
```bash
|
||||
# Check current deployment status
|
||||
npx hardhat upgrade:status --contract IdentityVerificationHub --network celo
|
||||
npx hardhat upgrade:prepare \
|
||||
--contract <ContractId> \
|
||||
--network <network> \
|
||||
[--changelog <message>] \
|
||||
[--dry-run] \
|
||||
[--skip-commit]
|
||||
```
|
||||
|
||||
# View version history
|
||||
### `upgrade:propose`
|
||||
|
||||
Creates a Safe multisig proposal for an already-deployed implementation.
|
||||
|
||||
```bash
|
||||
npx hardhat upgrade:propose \
|
||||
--contract <ContractId> \
|
||||
--network <network> \
|
||||
[--dry-run]
|
||||
```
|
||||
|
||||
### `upgrade:status`
|
||||
|
||||
Check current deployment status for a contract.
|
||||
|
||||
```bash
|
||||
npx hardhat upgrade:status --contract IdentityVerificationHub --network celo
|
||||
```
|
||||
|
||||
### `upgrade:history`
|
||||
|
||||
View the full version history for a contract.
|
||||
|
||||
```bash
|
||||
npx hardhat upgrade:history --contract IdentityVerificationHub
|
||||
```
|
||||
|
||||
@@ -68,17 +110,19 @@ npx hardhat upgrade:history --contract IdentityVerificationHub
|
||||
│ 1. UPDATE CONTRACT CODE │
|
||||
│ - Make your changes │
|
||||
│ - Update @custom:version in NatSpec │
|
||||
│ - Increment reinitializer(N) modifier │
|
||||
│ - Increment reinitializer(N) modifier (if new storage/init) │
|
||||
│ - Add new storage fields at END of struct only │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 2. RUN: npx hardhat upgrade --contract X --network Y --changelog Z │
|
||||
│ - Validates all safety checks │
|
||||
│ 2. DRY RUN: npx hardhat upgrade --contract X --network Y --dry-run │
|
||||
│ - Validates all safety checks without deploying │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 3. DEPLOY: npx hardhat upgrade --contract X --network Y --changelog │
|
||||
│ - Deploys new implementation │
|
||||
│ - Updates registry.json │
|
||||
│ - Creates git commit + tag │
|
||||
│ - Creates Safe proposal │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 3. MULTISIG APPROVAL │
|
||||
│ 4. MULTISIG APPROVAL │
|
||||
│ - Signers review in Safe UI │
|
||||
│ - Once threshold met, click Execute │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
@@ -98,7 +142,7 @@ contract MyContract is ImplRoot {
|
||||
uint256 newField; // <-- Add new fields at end only
|
||||
}
|
||||
|
||||
// Increment reinitializer(N) for each upgrade
|
||||
// Increment reinitializer(N) for each upgrade that needs initialization
|
||||
function initialize(...) external reinitializer(13) {
|
||||
// Initialize new fields if needed
|
||||
MyStorage storage $ = _getMyStorage();
|
||||
@@ -109,31 +153,66 @@ contract MyContract is ImplRoot {
|
||||
}
|
||||
```
|
||||
|
||||
Note: If an upgrade is code-only (no new storage, no initialization needed), you can bump `@custom:version` without
|
||||
adding a new `reinitializer`. The script handles both cases.
|
||||
|
||||
## Supported Contracts
|
||||
|
||||
| Contract ID | Source Contract | Type | Notes |
|
||||
| ------------------------- | ----------------------------- | --------------- | ---------------------------------------- |
|
||||
| `IdentityVerificationHub` | IdentityVerificationHubImplV2 | UUPS Proxy | Main verification hub |
|
||||
| `IdentityRegistry` | IdentityRegistryImplV1 | UUPS Proxy | Passport registry (links PoseidonT3) |
|
||||
| `IdentityRegistryIdCard` | IdentityRegistryIdCardImplV1 | UUPS Proxy | EU ID Card registry (links PoseidonT3) |
|
||||
| `IdentityRegistryAadhaar` | IdentityRegistryAadhaarImplV1 | UUPS Proxy | Aadhaar registry (links PoseidonT3) |
|
||||
| `IdentityRegistryKyc` | IdentityRegistryKycImplV1 | UUPS Proxy | KYC registry (links PoseidonT3) |
|
||||
| `PCR0Manager` | PCR0Manager | Non-upgradeable | TEE PCR0 value management (tracked only) |
|
||||
| `VerifyAll` | VerifyAll | Non-upgradeable | SDK verification helper (tracked only) |
|
||||
| `DummyContract` | DummyContract | UUPS Proxy | Testing contract |
|
||||
|
||||
The four registry contracts (Passport, IdCard, Aadhaar, KYC) all use the PoseidonT3 library for their internal Lean IMT.
|
||||
The upgrade script automatically deploys and links PoseidonT3 when deploying a new implementation for these contracts.
|
||||
|
||||
## Supported Networks
|
||||
|
||||
| Network | Chain ID | Governance | Use Case |
|
||||
| -------------- | -------- | ---------------------------- | ---------- |
|
||||
| `celo` | 42220 | 3/5 security, 2/5 operations | Production |
|
||||
| `celo-sepolia` | 11142220 | 1/1 single signer | Testnet |
|
||||
|
||||
## Configuration
|
||||
|
||||
### Deployment Registry
|
||||
|
||||
The registry (`deployments/registry.json`) tracks:
|
||||
|
||||
- Contract definitions (source contract name, type)
|
||||
- Proxy addresses per network
|
||||
- Current versions
|
||||
- Implementation history
|
||||
- Git commits and tags
|
||||
- Current versions and implementation addresses
|
||||
- Governance multisig configuration per network
|
||||
- Full version history with deployment details and git commits
|
||||
|
||||
### Governance Configuration
|
||||
|
||||
Multisig addresses are configured in `deployments/registry.json`:
|
||||
Multisig addresses are configured per network in `deployments/registry.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"networks": {
|
||||
"celo": {
|
||||
"governance": {
|
||||
"securityMultisig": "0x...",
|
||||
"operationsMultisig": "0x...",
|
||||
"securityMultisig": "0x738f...",
|
||||
"operationsMultisig": "0x067b...",
|
||||
"securityThreshold": "3/5",
|
||||
"operationsThreshold": "2/5"
|
||||
}
|
||||
},
|
||||
"celo-sepolia": {
|
||||
"governance": {
|
||||
"securityMultisig": "0x82D8...",
|
||||
"operationsMultisig": "0x82D8...",
|
||||
"securityThreshold": "1/1",
|
||||
"operationsThreshold": "1/1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,36 +223,30 @@ Multisig addresses are configured in `deployments/registry.json`:
|
||||
Required for deployments:
|
||||
|
||||
```bash
|
||||
PRIVATE_KEY=0x... # Deployer private key
|
||||
CELO_RPC_URL=https://... # RPC endpoint
|
||||
PRIVATE_KEY=0x... # Deployer private key
|
||||
CELO_RPC_URL=https://... # Celo mainnet RPC endpoint
|
||||
CELO_SEPOLIA_RPC_URL=https://... # Celo Sepolia RPC endpoint
|
||||
```
|
||||
|
||||
## Supported Contracts
|
||||
|
||||
| Contract ID | Contract Name | Type |
|
||||
| ------------------------- | ----------------------------- | ---------- |
|
||||
| `IdentityVerificationHub` | IdentityVerificationHubImplV2 | UUPS Proxy |
|
||||
| `IdentityRegistry` | IdentityRegistryImplV1 | UUPS Proxy |
|
||||
| `IdentityRegistryIdCard` | IdentityRegistryIdCardImplV1 | UUPS Proxy |
|
||||
| `IdentityRegistryAadhaar` | IdentityRegistryAadhaarImplV1 | UUPS Proxy |
|
||||
|
||||
## Safety Checks
|
||||
|
||||
| Check | What it Does | Failure Behavior |
|
||||
| ---------------------- | ------------------------------------------- | -------------------- |
|
||||
| Version validation | Ensures semantic version increment | Blocks upgrade |
|
||||
| Reinitializer check | Verifies `reinitializer(N)` matches version | Blocks upgrade |
|
||||
| Storage layout | Detects breaking storage changes | Blocks upgrade |
|
||||
| Bytecode comparison | Warns if code unchanged | Prompts confirmation |
|
||||
| Safe role verification | Confirms Safe has SECURITY_ROLE | Blocks upgrade |
|
||||
| Constructor check | Flags `_disableInitializers()` | Prompts confirmation |
|
||||
| Check | What it Does | Failure Behavior |
|
||||
| ---------------------- | ---------------------------------------------------- | -------------------- |
|
||||
| Version validation | Ensures semantic version increment vs registry | Blocks upgrade |
|
||||
| Reinitializer check | Verifies `reinitializer(N)` matches expected version | Blocks upgrade |
|
||||
| Storage layout | Detects breaking storage changes | Blocks upgrade |
|
||||
| Bytecode comparison | Warns if code unchanged from current impl | Prompts confirmation |
|
||||
| Safe role verification | Confirms Safe has SECURITY_ROLE on proxy | Blocks upgrade |
|
||||
| Constructor check | Flags `_disableInitializers()` | Prompts confirmation |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
| ----------------------------- | ----------------------------------------- |
|
||||
| "Version matches current" | Update `@custom:version` in contract |
|
||||
| "Reinitializer mismatch" | Update `reinitializer(N)` to next version |
|
||||
| "Storage layout incompatible" | Don't remove/reorder storage variables |
|
||||
| "Safe not indexed" | Submit manually via Safe UI |
|
||||
| "Bytecode unchanged" | Ensure you saved contract changes |
|
||||
| Issue | Solution |
|
||||
| -------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| "Version matches current" | Update `@custom:version` in contract NatSpec |
|
||||
| "Reinitializer mismatch" | Update `reinitializer(N)` to next version |
|
||||
| "Storage layout incompatible" | Don't remove/reorder storage variables |
|
||||
| "Safe not indexed" | Submit manually via Safe UI |
|
||||
| "Bytecode unchanged" | Ensure you saved contract changes |
|
||||
| "No proxy deployed for X on network Y" | Add the contract's proxy address to `registry.json` under `networks.<network>.deployments` |
|
||||
| "Invalid contract" | Contract ID not in `CONTRACT_IDS` array in `types.ts` |
|
||||
|
||||
@@ -202,7 +202,8 @@ task("upgrade:prepare", "Validate and deploy a new implementation contract")
|
||||
} else if (
|
||||
contractName === "IdentityRegistryImplV1" ||
|
||||
contractName === "IdentityRegistryIdCardImplV1" ||
|
||||
contractName === "IdentityRegistryAadhaarImplV1"
|
||||
contractName === "IdentityRegistryAadhaarImplV1" ||
|
||||
contractName === "IdentityRegistryKycImplV1"
|
||||
) {
|
||||
const PoseidonT3 = await hre.ethers.getContractFactory("PoseidonT3");
|
||||
const poseidonT3 = await PoseidonT3.deploy();
|
||||
|
||||
@@ -11,6 +11,7 @@ export const CONTRACT_IDS = [
|
||||
"IdentityRegistry",
|
||||
"IdentityRegistryIdCard",
|
||||
"IdentityRegistryAadhaar",
|
||||
"IdentityRegistryKyc",
|
||||
"PCR0Manager",
|
||||
"VerifyAll",
|
||||
"DummyContract",
|
||||
|
||||
@@ -426,7 +426,8 @@ task("upgrade", "Deploy new implementation and create Safe proposal for upgrade"
|
||||
} else if (
|
||||
contractName === "IdentityRegistryImplV1" ||
|
||||
contractName === "IdentityRegistryIdCardImplV1" ||
|
||||
contractName === "IdentityRegistryAadhaarImplV1"
|
||||
contractName === "IdentityRegistryAadhaarImplV1" ||
|
||||
contractName === "IdentityRegistryKycImplV1"
|
||||
) {
|
||||
const PoseidonT3 = await hre.ethers.getContractFactory("PoseidonT3");
|
||||
const poseidonT3 = await PoseidonT3.deploy();
|
||||
|
||||
482
contracts/test/v2/ofacProofUpdate.test.ts
Normal file
482
contracts/test/v2/ofacProofUpdate.test.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
import { ethers } from "hardhat";
|
||||
import { deploySystemFixturesV2 } from "../utils/deploymentV2";
|
||||
import { DeployedActorsV2 } from "../utils/types";
|
||||
import { expect } from "chai";
|
||||
|
||||
function getCurrentDateDigitsYYMMDDHHMMSS(hoursOffset: number = 0): bigint[] {
|
||||
const now = new Date();
|
||||
if (hoursOffset !== 0) {
|
||||
now.setUTCHours(now.getUTCHours() + hoursOffset);
|
||||
}
|
||||
const pad2 = (n: number) => n.toString().padStart(2, "0");
|
||||
const yy = pad2(now.getUTCFullYear() % 100);
|
||||
const mm = pad2(now.getUTCMonth() + 1);
|
||||
const dd = pad2(now.getUTCDate());
|
||||
const hh = pad2(now.getUTCHours());
|
||||
const min = pad2(now.getUTCMinutes());
|
||||
const ss = pad2(now.getUTCSeconds());
|
||||
return `${yy}${mm}${dd}${hh}${min}${ss}`.split("").map(Number).map(BigInt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs a uint256 value into field elements as a 64-character hex string.
|
||||
* Mirrors how the GCP JWT circuit outputs values in pubSignals[1-3].
|
||||
*/
|
||||
function packUint256ToHexFields(value: bigint): [bigint, bigint, bigint] {
|
||||
const hexStr = value.toString(16).padStart(64, "0");
|
||||
const bytes = Buffer.from(hexStr, "utf8");
|
||||
|
||||
let p0 = 0n,
|
||||
p1 = 0n,
|
||||
p2 = 0n;
|
||||
|
||||
for (let i = 0; i < Math.min(31, bytes.length); i++) {
|
||||
p0 |= BigInt(bytes[i]) << BigInt(i * 8);
|
||||
}
|
||||
for (let i = 31; i < Math.min(62, bytes.length); i++) {
|
||||
p1 |= BigInt(bytes[i]) << BigInt((i - 31) * 8);
|
||||
}
|
||||
for (let i = 62; i < Math.min(93, bytes.length); i++) {
|
||||
p2 |= BigInt(bytes[i]) << BigInt((i - 62) * 8);
|
||||
}
|
||||
|
||||
return [p0, p1, p2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes sha256(abi.encodePacked(roots...)) for a set of roots.
|
||||
* This is the nonce used in the proof for each registry.
|
||||
*/
|
||||
function computeRootsHash(roots: bigint[]): string {
|
||||
const types = roots.map(() => "uint256");
|
||||
return ethers.sha256(ethers.solidityPacked(types, roots));
|
||||
}
|
||||
|
||||
describe("OFAC Proof Update test", function () {
|
||||
this.timeout(0);
|
||||
|
||||
let deployedActors: DeployedActorsV2;
|
||||
let mockVerifier: any;
|
||||
|
||||
const GCP_ROOT_CA_PUBKEY_HASH = 21107503781769611051785921462832133421817512022858926231578334326320168810501n;
|
||||
|
||||
// Test image hash that unpacks to: d2221a0ee83901980c607ceff2edbedf3f6ce5f437eafa5d89be39e9e7487c04
|
||||
const testImageHash = {
|
||||
p0: 177384435506496807268973340845468654286294928521500580044819492874465981028n,
|
||||
p1: 175298970718174405520284770870231222447414486446296682893283627688949855078n,
|
||||
p2: 13360n,
|
||||
};
|
||||
|
||||
const mockProof = {
|
||||
a: [1n, 2n] as [bigint, bigint],
|
||||
b: [
|
||||
[1n, 2n],
|
||||
[3n, 4n],
|
||||
] as [[bigint, bigint], [bigint, bigint]],
|
||||
c: [1n, 2n] as [bigint, bigint],
|
||||
};
|
||||
|
||||
// Test OFAC roots (arbitrary non-zero values)
|
||||
const passportRoots = [100n, 200n, 300n];
|
||||
const aadhaarRoots = [400n, 500n];
|
||||
const idCardRoots = [600n, 700n];
|
||||
const kycRoots = [800n, 900n];
|
||||
|
||||
before(async () => {
|
||||
deployedActors = await deploySystemFixturesV2();
|
||||
const [deployer] = await ethers.getSigners();
|
||||
|
||||
// Deploy fresh MockGCPJWTVerifier
|
||||
const MockVerifierFactory = await ethers.getContractFactory("MockGCPJWTVerifier");
|
||||
mockVerifier = await MockVerifierFactory.deploy();
|
||||
await mockVerifier.waitForDeployment();
|
||||
|
||||
// Register test PCR0 image hash
|
||||
const pcr0Bytes = ethers.getBytes("0xd2221a0ee83901980c607ceff2edbedf3f6ce5f437eafa5d89be39e9e7487c04");
|
||||
await deployedActors.pcr0Manager.addPCR0(pcr0Bytes);
|
||||
|
||||
// Configure OFAC proof infrastructure on non-KYC registries via reinitializer(3)
|
||||
await deployedActors.registry.initializeOfacProof(
|
||||
mockVerifier.target,
|
||||
deployedActors.pcr0Manager.target,
|
||||
GCP_ROOT_CA_PUBKEY_HASH,
|
||||
deployer.address,
|
||||
);
|
||||
await deployedActors.registryId.initializeOfacProof(
|
||||
mockVerifier.target,
|
||||
deployedActors.pcr0Manager.target,
|
||||
GCP_ROOT_CA_PUBKEY_HASH,
|
||||
deployer.address,
|
||||
);
|
||||
await deployedActors.registryAadhaar.initializeOfacProof(
|
||||
mockVerifier.target,
|
||||
deployedActors.pcr0Manager.target,
|
||||
GCP_ROOT_CA_PUBKEY_HASH,
|
||||
deployer.address,
|
||||
);
|
||||
|
||||
// Configure KYC registry (already has pcr0Manager from init)
|
||||
await deployedActors.registryKyc.updateGCPJWTVerifier(mockVerifier.target);
|
||||
await deployedActors.registryKyc.updateGCPRootCAPubkeyHash(GCP_ROOT_CA_PUBKEY_HASH);
|
||||
await deployedActors.registryKyc.updateTEE(deployer.address);
|
||||
|
||||
console.log("OFAC proof test setup completed");
|
||||
});
|
||||
|
||||
/**
|
||||
* Builds pubSignals for a given set of roots (nonce = sha256 of the roots).
|
||||
*/
|
||||
function buildPubSignals(roots: bigint[], hoursOffset = 0): bigint[] {
|
||||
const rootsHash = computeRootsHash(roots);
|
||||
const [p0, p1, p2] = packUint256ToHexFields(BigInt(rootsHash));
|
||||
return [
|
||||
GCP_ROOT_CA_PUBKEY_HASH,
|
||||
p0,
|
||||
p1,
|
||||
p2,
|
||||
0n, // unused
|
||||
testImageHash.p0,
|
||||
testImageHash.p1,
|
||||
testImageHash.p2,
|
||||
...getCurrentDateDigitsYYMMDDHHMMSS(hoursOffset),
|
||||
];
|
||||
}
|
||||
|
||||
describe("Successful proof-based updates", () => {
|
||||
let snapshotId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ethers.provider.send("evm_revert", [snapshotId]);
|
||||
});
|
||||
|
||||
it("should update all 4 registries with valid proof", async () => {
|
||||
// Update Passport (3 roots)
|
||||
await expect(
|
||||
deployedActors.registry.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(passportRoots),
|
||||
passportRoots,
|
||||
),
|
||||
).to.emit(deployedActors.registry, "OfacRootsUpdatedWithProof");
|
||||
|
||||
// Update Aadhaar (2 roots)
|
||||
await expect(
|
||||
deployedActors.registryAadhaar.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(aadhaarRoots),
|
||||
aadhaarRoots,
|
||||
),
|
||||
).to.emit(deployedActors.registryAadhaar, "OfacRootsUpdatedWithProof");
|
||||
|
||||
// Update ID Card (2 roots)
|
||||
await expect(
|
||||
deployedActors.registryId.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(idCardRoots),
|
||||
idCardRoots,
|
||||
),
|
||||
).to.emit(deployedActors.registryId, "OfacRootsUpdatedWithProof");
|
||||
|
||||
// Update KYC (2 roots)
|
||||
await expect(
|
||||
deployedActors.registryKyc.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(kycRoots),
|
||||
kycRoots,
|
||||
),
|
||||
).to.emit(deployedActors.registryKyc, "OfacRootsUpdatedWithProof");
|
||||
|
||||
// Verify Passport roots
|
||||
expect(await deployedActors.registry.getNameAndDobOfacRoot()).to.equal(passportRoots[0]);
|
||||
expect(await deployedActors.registry.getNameAndYobOfacRoot()).to.equal(passportRoots[1]);
|
||||
expect(await deployedActors.registry.getPassportNoOfacRoot()).to.equal(passportRoots[2]);
|
||||
|
||||
// Verify Aadhaar roots
|
||||
expect(await deployedActors.registryAadhaar.getNameAndDobOfacRoot()).to.equal(aadhaarRoots[0]);
|
||||
expect(await deployedActors.registryAadhaar.getNameAndYobOfacRoot()).to.equal(aadhaarRoots[1]);
|
||||
|
||||
// Verify ID Card roots
|
||||
expect(await deployedActors.registryId.getNameAndDobOfacRoot()).to.equal(idCardRoots[0]);
|
||||
expect(await deployedActors.registryId.getNameAndYobOfacRoot()).to.equal(idCardRoots[1]);
|
||||
|
||||
// Verify KYC roots
|
||||
expect(await deployedActors.registryKyc.getNameAndDobOfacRoot()).to.equal(kycRoots[0]);
|
||||
expect(await deployedActors.registryKyc.getNameAndYobOfacRoot()).to.equal(kycRoots[1]);
|
||||
});
|
||||
|
||||
it("should reject non-TEE caller even with valid proof", async () => {
|
||||
await expect(
|
||||
deployedActors.registry
|
||||
.connect(deployedActors.user1)
|
||||
.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(passportRoots),
|
||||
passportRoots,
|
||||
),
|
||||
).to.be.revertedWithCustomError(deployedActors.registry, "ONLY_TEE_CAN_ACCESS");
|
||||
});
|
||||
|
||||
it("should emit individual root update events", async () => {
|
||||
await expect(
|
||||
deployedActors.registry.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(passportRoots),
|
||||
passportRoots,
|
||||
),
|
||||
)
|
||||
.to.emit(deployedActors.registry, "NameAndDobOfacRootUpdated")
|
||||
.withArgs(passportRoots[0])
|
||||
.and.to.emit(deployedActors.registry, "NameAndYobOfacRootUpdated")
|
||||
.withArgs(passportRoots[1])
|
||||
.and.to.emit(deployedActors.registry, "PassportNoOfacRootUpdated")
|
||||
.withArgs(passportRoots[2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error cases", () => {
|
||||
let snapshotId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ethers.provider.send("evm_revert", [snapshotId]);
|
||||
});
|
||||
|
||||
it("should revert with InvalidRootsCount when wrong number of roots for Passport", async () => {
|
||||
const wrongRoots = [100n, 200n];
|
||||
// Passport expects 3 roots, pass 2
|
||||
await expect(
|
||||
deployedActors.registry.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(wrongRoots),
|
||||
wrongRoots,
|
||||
),
|
||||
).to.be.revertedWithCustomError(deployedActors.registry, "InvalidRootsCount");
|
||||
});
|
||||
|
||||
it("should revert with InvalidRootsCount when wrong number of roots for KYC", async () => {
|
||||
const wrongRoots = [800n, 900n, 1000n];
|
||||
// KYC expects 2 roots, pass 3
|
||||
await expect(
|
||||
deployedActors.registryKyc.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(wrongRoots),
|
||||
wrongRoots,
|
||||
),
|
||||
).to.be.revertedWithCustomError(deployedActors.registryKyc, "InvalidRootsCount");
|
||||
});
|
||||
|
||||
it("should revert with INVALID_PROOF when verifier rejects proof", async () => {
|
||||
await mockVerifier.setShouldVerify(false);
|
||||
|
||||
await expect(
|
||||
deployedActors.registry.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(passportRoots),
|
||||
passportRoots,
|
||||
),
|
||||
).to.be.revertedWithCustomError(deployedActors.registry, "INVALID_PROOF");
|
||||
|
||||
// Reset mock verifier
|
||||
await mockVerifier.setShouldVerify(true);
|
||||
});
|
||||
|
||||
it("should revert with INVALID_ROOT_CA when rootCA hash does not match", async () => {
|
||||
const rootsHash = computeRootsHash(passportRoots);
|
||||
const [p0, p1, p2] = packUint256ToHexFields(BigInt(rootsHash));
|
||||
|
||||
// Use wrong rootCA hash in pubSignals[0]
|
||||
const badPubSignals: bigint[] = [
|
||||
12345n, // wrong rootCA
|
||||
p0,
|
||||
p1,
|
||||
p2,
|
||||
0n,
|
||||
testImageHash.p0,
|
||||
testImageHash.p1,
|
||||
testImageHash.p2,
|
||||
...getCurrentDateDigitsYYMMDDHHMMSS(),
|
||||
];
|
||||
|
||||
await expect(
|
||||
deployedActors.registry.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
badPubSignals,
|
||||
passportRoots,
|
||||
),
|
||||
).to.be.revertedWithCustomError(deployedActors.registry, "INVALID_ROOT_CA");
|
||||
});
|
||||
|
||||
it("should revert with INVALID_IMAGE when image hash not in PCR0Manager", async () => {
|
||||
const rootsHash = computeRootsHash(passportRoots);
|
||||
const [p0, p1, p2] = packUint256ToHexFields(BigInt(rootsHash));
|
||||
|
||||
// Use unknown image hash in pubSignals[5-7]
|
||||
const badPubSignals: bigint[] = [
|
||||
GCP_ROOT_CA_PUBKEY_HASH,
|
||||
p0,
|
||||
p1,
|
||||
p2,
|
||||
0n,
|
||||
99n, // bad image hash
|
||||
99n,
|
||||
99n,
|
||||
...getCurrentDateDigitsYYMMDDHHMMSS(),
|
||||
];
|
||||
|
||||
await expect(
|
||||
deployedActors.registry.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
badPubSignals,
|
||||
passportRoots,
|
||||
),
|
||||
).to.be.revertedWithCustomError(deployedActors.registry, "INVALID_IMAGE");
|
||||
});
|
||||
|
||||
it("should revert with INVALID_TIMESTAMP when timestamp is stale (2h past)", async () => {
|
||||
await expect(
|
||||
deployedActors.registry.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(passportRoots, -2),
|
||||
passportRoots,
|
||||
),
|
||||
).to.be.revertedWithCustomError(deployedActors.registry, "INVALID_TIMESTAMP");
|
||||
});
|
||||
|
||||
it("should revert with INVALID_TIMESTAMP when timestamp is too far in future (2h)", async () => {
|
||||
await expect(
|
||||
deployedActors.registry.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(passportRoots, 2),
|
||||
passportRoots,
|
||||
),
|
||||
).to.be.revertedWithCustomError(deployedActors.registry, "INVALID_TIMESTAMP");
|
||||
});
|
||||
|
||||
it("should revert with InvalidRootsHash when roots don't match proof nonce", async () => {
|
||||
// Build pubSignals with correct roots, but pass different roots to the contract
|
||||
const tamperedRoots = [999n, 888n, 777n];
|
||||
|
||||
await expect(
|
||||
deployedActors.registry.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(passportRoots),
|
||||
tamperedRoots,
|
||||
),
|
||||
).to.be.revertedWithCustomError(deployedActors.registry, "InvalidRootsHash");
|
||||
});
|
||||
|
||||
it("should revert on each registry type with InvalidRootsCount", async () => {
|
||||
// Aadhaar expects 2 roots, pass 1
|
||||
const oneRoot = [400n];
|
||||
await expect(
|
||||
deployedActors.registryAadhaar.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(oneRoot),
|
||||
oneRoot,
|
||||
),
|
||||
).to.be.revertedWithCustomError(deployedActors.registryAadhaar, "InvalidRootsCount");
|
||||
|
||||
// ID Card expects 2 roots, pass 3
|
||||
const threeRoots = [600n, 700n, 800n];
|
||||
await expect(
|
||||
deployedActors.registryId.updateOfacRootsWithProof(
|
||||
mockProof.a,
|
||||
mockProof.b,
|
||||
mockProof.c,
|
||||
buildPubSignals(threeRoots),
|
||||
threeRoots,
|
||||
),
|
||||
).to.be.revertedWithCustomError(deployedActors.registryId, "InvalidRootsCount");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Config setters (non-KYC registries)", () => {
|
||||
let snapshotId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ethers.provider.send("evm_revert", [snapshotId]);
|
||||
});
|
||||
|
||||
it("should allow SECURITY_ROLE to update GCP JWT verifier on Passport registry", async () => {
|
||||
const newVerifier = ethers.Wallet.createRandom().address;
|
||||
await expect(deployedActors.registry.updateGCPJWTVerifier(newVerifier))
|
||||
.to.emit(deployedActors.registry, "GCPJWTVerifierUpdated")
|
||||
.withArgs(newVerifier);
|
||||
});
|
||||
|
||||
it("should allow SECURITY_ROLE to update PCR0Manager on Passport registry", async () => {
|
||||
const newManager = ethers.Wallet.createRandom().address;
|
||||
await expect(deployedActors.registry.updatePCR0Manager(newManager))
|
||||
.to.emit(deployedActors.registry, "PCR0ManagerUpdated")
|
||||
.withArgs(newManager);
|
||||
});
|
||||
|
||||
it("should allow SECURITY_ROLE to update GCP root CA hash on Passport registry", async () => {
|
||||
const newHash = 99999n;
|
||||
await expect(deployedActors.registry.updateGCPRootCAPubkeyHash(newHash))
|
||||
.to.emit(deployedActors.registry, "GCPRootCAPubkeyHashUpdated")
|
||||
.withArgs(newHash);
|
||||
});
|
||||
|
||||
it("should not allow non-SECURITY_ROLE to update GCP JWT verifier", async () => {
|
||||
await expect(
|
||||
deployedActors.registry
|
||||
.connect(deployedActors.user1)
|
||||
.updateGCPJWTVerifier(ethers.Wallet.createRandom().address),
|
||||
).to.be.revertedWithCustomError(deployedActors.registry, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
|
||||
it("should not allow non-SECURITY_ROLE to update PCR0Manager", async () => {
|
||||
await expect(
|
||||
deployedActors.registry.connect(deployedActors.user1).updatePCR0Manager(ethers.Wallet.createRandom().address),
|
||||
).to.be.revertedWithCustomError(deployedActors.registry, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
|
||||
it("should not allow non-SECURITY_ROLE to update GCP root CA hash", async () => {
|
||||
await expect(
|
||||
deployedActors.registry.connect(deployedActors.user1).updateGCPRootCAPubkeyHash(99999n),
|
||||
).to.be.revertedWithCustomError(deployedActors.registry, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
});
|
||||
});
|
||||
332
contracts/test/v2/ofacRollingWindow.test.ts
Normal file
332
contracts/test/v2/ofacRollingWindow.test.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
import { ethers } from "hardhat";
|
||||
import { expect } from "chai";
|
||||
|
||||
/**
|
||||
* Lightweight deployment of just the 4 registry proxies — no verifiers,
|
||||
* no hub, no staging artifacts required.
|
||||
*/
|
||||
async function deployRegistries() {
|
||||
const [owner] = await ethers.getSigners();
|
||||
|
||||
// Deploy PoseidonT3 (required by all registries)
|
||||
const poseidonT3 = await (await ethers.getContractFactory("PoseidonT3")).connect(owner).deploy();
|
||||
await poseidonT3.waitForDeployment();
|
||||
|
||||
// Deploy PCR0Manager (required by KYC init)
|
||||
const pcr0Manager = await (await ethers.getContractFactory("PCR0Manager")).connect(owner).deploy();
|
||||
await pcr0Manager.waitForDeployment();
|
||||
|
||||
const libs = { libraries: { PoseidonT3: poseidonT3.target } };
|
||||
const temporaryHubAddress = "0x0000000000000000000000000000000000000000";
|
||||
|
||||
// Helper: deploy impl + proxy, return proxy with impl ABI
|
||||
async function deployRegistry(implName: string, initArgs: any[]) {
|
||||
const ImplFactory = await ethers.getContractFactory(implName, libs);
|
||||
const impl = await ImplFactory.connect(owner).deploy();
|
||||
await impl.waitForDeployment();
|
||||
|
||||
const initData = impl.interface.encodeFunctionData("initialize", initArgs);
|
||||
const ProxyFactory = await ethers.getContractFactory("IdentityRegistry");
|
||||
const proxy = await ProxyFactory.connect(owner).deploy(impl.target, initData);
|
||||
await proxy.waitForDeployment();
|
||||
|
||||
return ethers.getContractAt(implName, proxy.target);
|
||||
}
|
||||
|
||||
const registry = await deployRegistry("IdentityRegistryImplV1", [temporaryHubAddress]);
|
||||
const registryId = await deployRegistry("IdentityRegistryIdCardImplV1", [temporaryHubAddress]);
|
||||
const registryAadhaar = await deployRegistry("IdentityRegistryAadhaarImplV1", [temporaryHubAddress]);
|
||||
const registryKyc = await deployRegistry("IdentityRegistryKycImplV1", [temporaryHubAddress, pcr0Manager.target]);
|
||||
|
||||
return { registry, registryId, registryAadhaar, registryKyc, owner };
|
||||
}
|
||||
|
||||
describe("OFAC Rolling Root Window test", function () {
|
||||
this.timeout(0);
|
||||
|
||||
let registry: any;
|
||||
let registryId: any;
|
||||
let registryAadhaar: any;
|
||||
let registryKyc: any;
|
||||
|
||||
const ROOT_A_DOB = 1000n;
|
||||
const ROOT_A_YOB = 2000n;
|
||||
const ROOT_A_PASSPORT = 3000n;
|
||||
|
||||
const ROOT_B_DOB = 4000n;
|
||||
const ROOT_B_YOB = 5000n;
|
||||
const ROOT_B_PASSPORT = 6000n;
|
||||
|
||||
const ROOT_C_DOB = 7000n;
|
||||
const ROOT_C_YOB = 8000n;
|
||||
const ROOT_C_PASSPORT = 9000n;
|
||||
|
||||
before(async () => {
|
||||
const d = await deployRegistries();
|
||||
registry = d.registry;
|
||||
registryId = d.registryId;
|
||||
registryAadhaar = d.registryAadhaar;
|
||||
registryKyc = d.registryKyc;
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// KYC Registry (2 roots: nameAndDob, nameAndYob)
|
||||
// ──────────────────────────────────────────────────────────
|
||||
describe("KYC Registry", () => {
|
||||
let snapshotId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ethers.provider.send("evm_revert", [snapshotId]);
|
||||
});
|
||||
|
||||
it("should accept current roots", async () => {
|
||||
await registryKyc.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registryKyc.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
expect(await registryKyc.checkOfacRoots(ROOT_A_DOB, ROOT_A_YOB)).to.be.true;
|
||||
});
|
||||
|
||||
it("should accept previous roots after one update", async () => {
|
||||
await registryKyc.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registryKyc.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
await registryKyc.updateNameAndDobOfacRoot(ROOT_B_DOB);
|
||||
await registryKyc.updateNameAndYobOfacRoot(ROOT_B_YOB);
|
||||
|
||||
// Current
|
||||
expect(await registryKyc.checkOfacRoots(ROOT_B_DOB, ROOT_B_YOB)).to.be.true;
|
||||
// Previous (rolling window)
|
||||
expect(await registryKyc.checkOfacRoots(ROOT_A_DOB, ROOT_A_YOB)).to.be.true;
|
||||
// Mixed current+previous
|
||||
expect(await registryKyc.checkOfacRoots(ROOT_A_DOB, ROOT_B_YOB)).to.be.true;
|
||||
expect(await registryKyc.checkOfacRoots(ROOT_B_DOB, ROOT_A_YOB)).to.be.true;
|
||||
});
|
||||
|
||||
it("should reject roots from 2 updates ago (window = 1)", async () => {
|
||||
await registryKyc.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registryKyc.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
await registryKyc.updateNameAndDobOfacRoot(ROOT_B_DOB);
|
||||
await registryKyc.updateNameAndYobOfacRoot(ROOT_B_YOB);
|
||||
|
||||
await registryKyc.updateNameAndDobOfacRoot(ROOT_C_DOB);
|
||||
await registryKyc.updateNameAndYobOfacRoot(ROOT_C_YOB);
|
||||
|
||||
expect(await registryKyc.checkOfacRoots(ROOT_C_DOB, ROOT_C_YOB)).to.be.true;
|
||||
expect(await registryKyc.checkOfacRoots(ROOT_B_DOB, ROOT_B_YOB)).to.be.true;
|
||||
expect(await registryKyc.checkOfacRoots(ROOT_A_DOB, ROOT_A_YOB)).to.be.false;
|
||||
});
|
||||
|
||||
it("should store previous root correctly via getters", async () => {
|
||||
await registryKyc.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registryKyc.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
expect(await registryKyc.getPrevNameAndDobOfacRoot()).to.equal(0n);
|
||||
expect(await registryKyc.getPrevNameAndYobOfacRoot()).to.equal(0n);
|
||||
|
||||
await registryKyc.updateNameAndDobOfacRoot(ROOT_B_DOB);
|
||||
await registryKyc.updateNameAndYobOfacRoot(ROOT_B_YOB);
|
||||
|
||||
expect(await registryKyc.getPrevNameAndDobOfacRoot()).to.equal(ROOT_A_DOB);
|
||||
expect(await registryKyc.getPrevNameAndYobOfacRoot()).to.equal(ROOT_A_YOB);
|
||||
expect(await registryKyc.getNameAndDobOfacRoot()).to.equal(ROOT_B_DOB);
|
||||
expect(await registryKyc.getNameAndYobOfacRoot()).to.equal(ROOT_B_YOB);
|
||||
});
|
||||
|
||||
it("should reject random roots", async () => {
|
||||
await registryKyc.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registryKyc.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
expect(await registryKyc.checkOfacRoots(99999n, 88888n)).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// Passport Registry (3 roots)
|
||||
// ──────────────────────────────────────────────────────────
|
||||
describe("Passport Registry", () => {
|
||||
let snapshotId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ethers.provider.send("evm_revert", [snapshotId]);
|
||||
});
|
||||
|
||||
it("should accept current roots", async () => {
|
||||
await registry.updatePassportNoOfacRoot(ROOT_A_PASSPORT);
|
||||
await registry.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registry.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
expect(await registry.checkOfacRoots(ROOT_A_PASSPORT, ROOT_A_DOB, ROOT_A_YOB)).to.be.true;
|
||||
});
|
||||
|
||||
it("should accept previous roots after one update", async () => {
|
||||
await registry.updatePassportNoOfacRoot(ROOT_A_PASSPORT);
|
||||
await registry.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registry.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
await registry.updatePassportNoOfacRoot(ROOT_B_PASSPORT);
|
||||
await registry.updateNameAndDobOfacRoot(ROOT_B_DOB);
|
||||
await registry.updateNameAndYobOfacRoot(ROOT_B_YOB);
|
||||
|
||||
expect(await registry.checkOfacRoots(ROOT_B_PASSPORT, ROOT_B_DOB, ROOT_B_YOB)).to.be.true;
|
||||
expect(await registry.checkOfacRoots(ROOT_A_PASSPORT, ROOT_A_DOB, ROOT_A_YOB)).to.be.true;
|
||||
// Mixed
|
||||
expect(await registry.checkOfacRoots(ROOT_A_PASSPORT, ROOT_B_DOB, ROOT_B_YOB)).to.be.true;
|
||||
});
|
||||
|
||||
it("should reject roots from 2 updates ago (window = 1)", async () => {
|
||||
await registry.updatePassportNoOfacRoot(ROOT_A_PASSPORT);
|
||||
await registry.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registry.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
await registry.updatePassportNoOfacRoot(ROOT_B_PASSPORT);
|
||||
await registry.updateNameAndDobOfacRoot(ROOT_B_DOB);
|
||||
await registry.updateNameAndYobOfacRoot(ROOT_B_YOB);
|
||||
|
||||
await registry.updatePassportNoOfacRoot(ROOT_C_PASSPORT);
|
||||
await registry.updateNameAndDobOfacRoot(ROOT_C_DOB);
|
||||
await registry.updateNameAndYobOfacRoot(ROOT_C_YOB);
|
||||
|
||||
expect(await registry.checkOfacRoots(ROOT_C_PASSPORT, ROOT_C_DOB, ROOT_C_YOB)).to.be.true;
|
||||
expect(await registry.checkOfacRoots(ROOT_B_PASSPORT, ROOT_B_DOB, ROOT_B_YOB)).to.be.true;
|
||||
expect(await registry.checkOfacRoots(ROOT_A_PASSPORT, ROOT_A_DOB, ROOT_A_YOB)).to.be.false;
|
||||
});
|
||||
|
||||
it("should store previous roots correctly via getters", async () => {
|
||||
await registry.updatePassportNoOfacRoot(ROOT_A_PASSPORT);
|
||||
await registry.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registry.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
await registry.updatePassportNoOfacRoot(ROOT_B_PASSPORT);
|
||||
await registry.updateNameAndDobOfacRoot(ROOT_B_DOB);
|
||||
await registry.updateNameAndYobOfacRoot(ROOT_B_YOB);
|
||||
|
||||
expect(await registry.getPrevPassportNoOfacRoot()).to.equal(ROOT_A_PASSPORT);
|
||||
expect(await registry.getPrevNameAndDobOfacRoot()).to.equal(ROOT_A_DOB);
|
||||
expect(await registry.getPrevNameAndYobOfacRoot()).to.equal(ROOT_A_YOB);
|
||||
});
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// ID Card Registry (2 roots)
|
||||
// ──────────────────────────────────────────────────────────
|
||||
describe("ID Card Registry", () => {
|
||||
let snapshotId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ethers.provider.send("evm_revert", [snapshotId]);
|
||||
});
|
||||
|
||||
it("should accept current roots", async () => {
|
||||
await registryId.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registryId.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
expect(await registryId.checkOfacRoots(ROOT_A_DOB, ROOT_A_YOB)).to.be.true;
|
||||
});
|
||||
|
||||
it("should accept previous roots after one update", async () => {
|
||||
await registryId.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registryId.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
await registryId.updateNameAndDobOfacRoot(ROOT_B_DOB);
|
||||
await registryId.updateNameAndYobOfacRoot(ROOT_B_YOB);
|
||||
|
||||
expect(await registryId.checkOfacRoots(ROOT_B_DOB, ROOT_B_YOB)).to.be.true;
|
||||
expect(await registryId.checkOfacRoots(ROOT_A_DOB, ROOT_A_YOB)).to.be.true;
|
||||
});
|
||||
|
||||
it("should reject roots from 2 updates ago", async () => {
|
||||
await registryId.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registryId.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
await registryId.updateNameAndDobOfacRoot(ROOT_B_DOB);
|
||||
await registryId.updateNameAndYobOfacRoot(ROOT_B_YOB);
|
||||
|
||||
await registryId.updateNameAndDobOfacRoot(ROOT_C_DOB);
|
||||
await registryId.updateNameAndYobOfacRoot(ROOT_C_YOB);
|
||||
|
||||
expect(await registryId.checkOfacRoots(ROOT_C_DOB, ROOT_C_YOB)).to.be.true;
|
||||
expect(await registryId.checkOfacRoots(ROOT_B_DOB, ROOT_B_YOB)).to.be.true;
|
||||
expect(await registryId.checkOfacRoots(ROOT_A_DOB, ROOT_A_YOB)).to.be.false;
|
||||
});
|
||||
|
||||
it("should store previous roots correctly via getters", async () => {
|
||||
await registryId.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registryId.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
await registryId.updateNameAndDobOfacRoot(ROOT_B_DOB);
|
||||
await registryId.updateNameAndYobOfacRoot(ROOT_B_YOB);
|
||||
|
||||
expect(await registryId.getPrevNameAndDobOfacRoot()).to.equal(ROOT_A_DOB);
|
||||
expect(await registryId.getPrevNameAndYobOfacRoot()).to.equal(ROOT_A_YOB);
|
||||
});
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// Aadhaar Registry (2 roots)
|
||||
// ──────────────────────────────────────────────────────────
|
||||
describe("Aadhaar Registry", () => {
|
||||
let snapshotId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ethers.provider.send("evm_revert", [snapshotId]);
|
||||
});
|
||||
|
||||
it("should accept current roots", async () => {
|
||||
await registryAadhaar.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registryAadhaar.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
expect(await registryAadhaar.checkOfacRoots(ROOT_A_DOB, ROOT_A_YOB)).to.be.true;
|
||||
});
|
||||
|
||||
it("should accept previous roots after one update", async () => {
|
||||
await registryAadhaar.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registryAadhaar.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
await registryAadhaar.updateNameAndDobOfacRoot(ROOT_B_DOB);
|
||||
await registryAadhaar.updateNameAndYobOfacRoot(ROOT_B_YOB);
|
||||
|
||||
expect(await registryAadhaar.checkOfacRoots(ROOT_B_DOB, ROOT_B_YOB)).to.be.true;
|
||||
expect(await registryAadhaar.checkOfacRoots(ROOT_A_DOB, ROOT_A_YOB)).to.be.true;
|
||||
});
|
||||
|
||||
it("should reject roots from 2 updates ago", async () => {
|
||||
await registryAadhaar.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registryAadhaar.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
await registryAadhaar.updateNameAndDobOfacRoot(ROOT_B_DOB);
|
||||
await registryAadhaar.updateNameAndYobOfacRoot(ROOT_B_YOB);
|
||||
|
||||
await registryAadhaar.updateNameAndDobOfacRoot(ROOT_C_DOB);
|
||||
await registryAadhaar.updateNameAndYobOfacRoot(ROOT_C_YOB);
|
||||
|
||||
expect(await registryAadhaar.checkOfacRoots(ROOT_C_DOB, ROOT_C_YOB)).to.be.true;
|
||||
expect(await registryAadhaar.checkOfacRoots(ROOT_B_DOB, ROOT_B_YOB)).to.be.true;
|
||||
expect(await registryAadhaar.checkOfacRoots(ROOT_A_DOB, ROOT_A_YOB)).to.be.false;
|
||||
});
|
||||
|
||||
it("should store previous roots correctly via getters", async () => {
|
||||
await registryAadhaar.updateNameAndDobOfacRoot(ROOT_A_DOB);
|
||||
await registryAadhaar.updateNameAndYobOfacRoot(ROOT_A_YOB);
|
||||
|
||||
await registryAadhaar.updateNameAndDobOfacRoot(ROOT_B_DOB);
|
||||
await registryAadhaar.updateNameAndYobOfacRoot(ROOT_B_YOB);
|
||||
|
||||
expect(await registryAadhaar.getPrevNameAndDobOfacRoot()).to.equal(ROOT_A_DOB);
|
||||
expect(await registryAadhaar.getPrevNameAndYobOfacRoot()).to.equal(ROOT_A_YOB);
|
||||
});
|
||||
});
|
||||
});
|
||||
172
contracts/test/v2/ofacUpgradePath.test.ts
Normal file
172
contracts/test/v2/ofacUpgradePath.test.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* OFAC Proof Update — Upgrade Path Test
|
||||
*
|
||||
* Validates that:
|
||||
* 1. A KYC proxy with the current implementation can be upgraded
|
||||
* 2. State (OFAC roots, IMT) survives the upgrade
|
||||
* 3. updateOfacRootsWithProof works after upgrade
|
||||
* 4. The new function is callable by non-owners (proof IS authorization)
|
||||
*
|
||||
* This test deploys a full system, sets some OFAC roots via the old setter
|
||||
* functions, then upgrades the implementation and verifies everything works
|
||||
* with the proof-based update path.
|
||||
*/
|
||||
|
||||
import { ethers } from "hardhat";
|
||||
import { deploySystemFixturesV2 } from "../utils/deploymentV2";
|
||||
import { DeployedActorsV2 } from "../utils/types";
|
||||
import { expect } from "chai";
|
||||
|
||||
const GCP_ROOT_CA_PUBKEY_HASH = 21107503781769611051785921462832133421817512022858926231578334326320168810501n;
|
||||
|
||||
function packUint256ToHexFields(value: bigint): [bigint, bigint, bigint] {
|
||||
const hexStr = value.toString(16).padStart(64, "0");
|
||||
const bytes = Buffer.from(hexStr, "utf8");
|
||||
let p0 = 0n,
|
||||
p1 = 0n,
|
||||
p2 = 0n;
|
||||
for (let i = 0; i < Math.min(31, bytes.length); i++) p0 |= BigInt(bytes[i]) << BigInt(i * 8);
|
||||
for (let i = 31; i < Math.min(62, bytes.length); i++) p1 |= BigInt(bytes[i]) << BigInt((i - 31) * 8);
|
||||
for (let i = 62; i < Math.min(93, bytes.length); i++) p2 |= BigInt(bytes[i]) << BigInt((i - 62) * 8);
|
||||
return [p0, p1, p2];
|
||||
}
|
||||
|
||||
function getCurrentDateDigitsYYMMDDHHMMSS(): bigint[] {
|
||||
const now = new Date();
|
||||
const pad2 = (n: number) => n.toString().padStart(2, "0");
|
||||
const yy = pad2(now.getUTCFullYear() % 100);
|
||||
const mm = pad2(now.getUTCMonth() + 1);
|
||||
const dd = pad2(now.getUTCDate());
|
||||
const hh = pad2(now.getUTCHours());
|
||||
const min = pad2(now.getUTCMinutes());
|
||||
const ss = pad2(now.getUTCSeconds());
|
||||
return `${yy}${mm}${dd}${hh}${min}${ss}`.split("").map(Number).map(BigInt);
|
||||
}
|
||||
|
||||
describe("OFAC Upgrade Path Test", function () {
|
||||
this.timeout(0);
|
||||
|
||||
let actors: DeployedActorsV2;
|
||||
let mockVerifier: any;
|
||||
|
||||
const testImageHash = {
|
||||
p0: 177384435506496807268973340845468654286294928521500580044819492874465981028n,
|
||||
p1: 175298970718174405520284770870231222447414486446296682893283627688949855078n,
|
||||
p2: 13360n,
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
actors = await deploySystemFixturesV2();
|
||||
|
||||
// Deploy fresh MockGCPJWTVerifier
|
||||
const MockVerifierFactory = await ethers.getContractFactory("MockGCPJWTVerifier");
|
||||
mockVerifier = await MockVerifierFactory.deploy();
|
||||
await mockVerifier.waitForDeployment();
|
||||
|
||||
// Register test PCR0 image hash
|
||||
const pcr0Bytes = ethers.getBytes("0xd2221a0ee83901980c607ceff2edbedf3f6ce5f437eafa5d89be39e9e7487c04");
|
||||
await actors.pcr0Manager.addPCR0(pcr0Bytes);
|
||||
});
|
||||
|
||||
it("should preserve state after upgrade and allow proof-based updates", async () => {
|
||||
// --- Phase 1: Set some OFAC roots using old setter functions ---
|
||||
const initialDobRoot = 12345n;
|
||||
const initialYobRoot = 67890n;
|
||||
|
||||
await actors.registryKyc.updateNameAndDobOfacRoot(initialDobRoot);
|
||||
await actors.registryKyc.updateNameAndYobOfacRoot(initialYobRoot);
|
||||
|
||||
// Verify old roots are set
|
||||
expect(await actors.registryKyc.getNameAndDobOfacRoot()).to.equal(initialDobRoot);
|
||||
expect(await actors.registryKyc.getNameAndYobOfacRoot()).to.equal(initialYobRoot);
|
||||
console.log(` Pre-upgrade OFAC roots: DOB=${initialDobRoot}, YOB=${initialYobRoot}`);
|
||||
|
||||
// --- Phase 2: Upgrade KYC implementation ---
|
||||
// Deploy new implementation (needs PoseidonT3 library linked)
|
||||
const KycFactory = await ethers.getContractFactory("IdentityRegistryKycImplV1", {
|
||||
libraries: {
|
||||
PoseidonT3: await actors.poseidonT3.getAddress(),
|
||||
},
|
||||
});
|
||||
const newImpl = await KycFactory.deploy();
|
||||
await newImpl.waitForDeployment();
|
||||
console.log(` New KYC impl deployed: ${newImpl.target}`);
|
||||
|
||||
// Get proxy address
|
||||
const proxyAddress = await actors.registryKyc.getAddress();
|
||||
|
||||
// Upgrade via upgradeToAndCall (no reinitializer data for KYC)
|
||||
await actors.registryKyc.upgradeToAndCall(newImpl.target, "0x");
|
||||
console.log(` Proxy upgraded to new implementation`);
|
||||
|
||||
// --- Phase 3: Verify state survived ---
|
||||
expect(await actors.registryKyc.getNameAndDobOfacRoot()).to.equal(initialDobRoot);
|
||||
expect(await actors.registryKyc.getNameAndYobOfacRoot()).to.equal(initialYobRoot);
|
||||
console.log(` State preserved: DOB=${initialDobRoot}, YOB=${initialYobRoot}`);
|
||||
|
||||
// --- Phase 4: Configure mock verifier and TEE address for proof-based updates ---
|
||||
const [deployer] = await ethers.getSigners();
|
||||
await actors.registryKyc.updateGCPJWTVerifier(mockVerifier.target);
|
||||
await actors.registryKyc.updateGCPRootCAPubkeyHash(GCP_ROOT_CA_PUBKEY_HASH);
|
||||
await actors.registryKyc.updateTEE(deployer.address);
|
||||
|
||||
// --- Phase 5: Call updateOfacRootsWithProof ---
|
||||
const kycRoots = [800n, 900n];
|
||||
|
||||
// Compute roots hash (nonce for the proof)
|
||||
const rootsHash = ethers.sha256(ethers.solidityPacked(["uint256", "uint256"], kycRoots));
|
||||
const [p0, p1, p2] = packUint256ToHexFields(BigInt(rootsHash));
|
||||
|
||||
const pubSignals: bigint[] = [
|
||||
GCP_ROOT_CA_PUBKEY_HASH,
|
||||
p0,
|
||||
p1,
|
||||
p2,
|
||||
0n,
|
||||
testImageHash.p0,
|
||||
testImageHash.p1,
|
||||
testImageHash.p2,
|
||||
...getCurrentDateDigitsYYMMDDHHMMSS(),
|
||||
];
|
||||
|
||||
const mockProof = {
|
||||
a: [1n, 2n] as [bigint, bigint],
|
||||
b: [
|
||||
[1n, 2n],
|
||||
[3n, 4n],
|
||||
] as [[bigint, bigint], [bigint, bigint]],
|
||||
c: [1n, 2n] as [bigint, bigint],
|
||||
};
|
||||
|
||||
// Call from TEE address (deployer)
|
||||
const proofTx = await actors.registryKyc
|
||||
.connect(deployer)
|
||||
.updateOfacRootsWithProof(mockProof.a, mockProof.b, mockProof.c, pubSignals, kycRoots);
|
||||
await proofTx.wait();
|
||||
|
||||
// --- Phase 6: Verify new current roots ---
|
||||
expect(await actors.registryKyc.getNameAndDobOfacRoot()).to.equal(800n);
|
||||
expect(await actors.registryKyc.getNameAndYobOfacRoot()).to.equal(900n);
|
||||
console.log(` Proof-based update succeeded: DOB=800, YOB=900`);
|
||||
|
||||
// --- Phase 6b: Verify prev* slots were rotated correctly ---
|
||||
expect(await actors.registryKyc.getPrevNameAndDobOfacRoot()).to.equal(initialDobRoot);
|
||||
expect(await actors.registryKyc.getPrevNameAndYobOfacRoot()).to.equal(initialYobRoot);
|
||||
console.log(` Rolling window: prev DOB=${initialDobRoot}, prev YOB=${initialYobRoot}`);
|
||||
|
||||
// --- Phase 6c: Verify checkOfacRoots accepts current and previous snapshots (not mixed) ---
|
||||
expect(await actors.registryKyc.checkOfacRoots(800n, 900n)).to.equal(true);
|
||||
expect(await actors.registryKyc.checkOfacRoots(initialDobRoot, initialYobRoot)).to.equal(true);
|
||||
expect(await actors.registryKyc.checkOfacRoots(800n, initialYobRoot)).to.equal(
|
||||
false,
|
||||
"mixed pair must be rejected",
|
||||
);
|
||||
|
||||
// --- Phase 7: Verify old setter functions still work after upgrade ---
|
||||
await actors.registryKyc.updateNameAndDobOfacRoot(11111n);
|
||||
expect(await actors.registryKyc.getNameAndDobOfacRoot()).to.equal(11111n);
|
||||
console.log(` Old setter still works after upgrade: DOB=11111`);
|
||||
|
||||
console.log(`\n ✅ Upgrade path test passed — state preserved, proof updates work, old setters still work`);
|
||||
});
|
||||
});
|
||||
87
docs/reviews/PR-1924-review-findings.md
Normal file
87
docs/reviews/PR-1924-review-findings.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# PR #1924 Review Findings
|
||||
|
||||
**PR:** Harden WebView bridge and asset serving across native shells
|
||||
**Branch:** `justin/address-wv-vulns`
|
||||
**Reviewed:** 2026-04-05
|
||||
**Last updated:** 2026-04-05
|
||||
|
||||
This document reflects the substantive, non-pedantic feedback on PR #1924 from both GitHub PR comments and manual code review.
|
||||
It intentionally excludes low-signal review noise such as docstring coverage, PR description nags, bot walkthrough summaries, and duplicate comments that collapse into the same work item.
|
||||
|
||||
---
|
||||
|
||||
## Resolved
|
||||
|
||||
### ~~1. Bundled Android entry path breaks relative asset loading~~
|
||||
|
||||
**Status:** Resolved — not a real issue.
|
||||
`BundledAssetPathHandler` receives only the URL path component (e.g. `/assets/app.js`), not the full navigation URL. The `/tunnel/tour/1` initial URL never reaches the asset handler. Asset requests correctly resolve to `self-wallet/assets/...` in the Android bundle. PR comment resolved.
|
||||
|
||||
### ~~6. iOS local asset server startup must fail closed~~
|
||||
|
||||
**Status:** Resolved — already addressed.
|
||||
`SelfWebViewHost` (native-shell-ios) uses a custom URL scheme handler (`SelfBundledAssetSchemeHandler`), not a local asset server. `WebViewProviderImpl` (self-sdk-swift) falls back to `bundledPort = 0` on server failure, which correctly rejects trust checks downstream. PR comments resolved.
|
||||
|
||||
### ~~7. SwiftPM resource path is declared but not populated by build automation~~
|
||||
|
||||
**Status:** Resolved — already addressed.
|
||||
`build-webview-bundle.sh` copies the generated bundle to the self-sdk-swift resources path. The directory exists and is populated. PR comments resolved.
|
||||
|
||||
### ~~2. Trust boundary is fail-open in MessageRouter APIs~~
|
||||
|
||||
**Status:** Resolved.
|
||||
Removed the default `isTrustedSource = true` value from the KMP, Android, and iOS routers, and updated callers/tests to pass trust explicitly.
|
||||
|
||||
### ~~3. Android bridge trust uses `webView.url` instead of callback origin~~
|
||||
|
||||
**Status:** Resolved.
|
||||
Both Android hosts now evaluate bridge trust from `WebViewCompat.addWebMessageListener`'s `sourceOrigin` callback parameter rather than re-reading `webView.url`.
|
||||
|
||||
### ~~4. iOS bridge trust is rechecked from `webView?.url` after the origin was already validated~~
|
||||
|
||||
**Status:** Resolved.
|
||||
`SelfWebViewHost` now passes `isTrustedSource: true` after `isTrustedBridgeFrameInfo()` succeeds, removing the race-prone recheck against `webView?.url`.
|
||||
|
||||
### ~~5. Bridge initialization does not fail closed when `WEB_MESSAGE_LISTENER` is unavailable~~
|
||||
|
||||
**Status:** Resolved.
|
||||
Both Android hosts now fail closed with a hard `check(...)` when `WEB_MESSAGE_LISTENER` is unavailable instead of loading a broken bridge.
|
||||
|
||||
### ~~8. iOS `loadHTMLString` base URL resolves relative assets against entry path~~
|
||||
|
||||
**Status:** Resolved.
|
||||
`SelfWebViewHost` already loads verified remote HTML with the configured `baseURL`, not the full `/tunnel/tour/1` entry URL, so relative asset resolution is anchored correctly at the remote app base.
|
||||
|
||||
### ~~9. Android allows Didit navigation in main frame; iOS restricts to subframes only~~
|
||||
|
||||
**Status:** Resolved.
|
||||
The Android native-shell and KMP hosts now reject Didit in `isAllowedNavigationUrl`, aligning main-frame behavior with the iOS restriction.
|
||||
|
||||
### ~~10. Duplicate constant in Android host~~
|
||||
|
||||
**Status:** Resolved.
|
||||
The current native Android host uses only `BUNDLED_ASSET_HOST`; the duplicate `BUNDLED_HOST` constant is no longer present.
|
||||
|
||||
### ~~11. iOS `navigationDelegate` set twice~~
|
||||
|
||||
**Status:** Resolved.
|
||||
The current `SelfWebViewHost` assigns `webView.navigationDelegate = self` only once.
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
- `cd packages/native-shell-android && ./gradlew test` — passed
|
||||
- `cd packages/kmp-sdk && ./gradlew :shared:jvmTest` — passed
|
||||
- `cd packages/native-shell-ios && swift test` — blocked by environment: SwiftPM cannot import `UIKit` in this shell (`no such module 'UIKit'`)
|
||||
|
||||
## Current Status
|
||||
|
||||
All substantive findings tracked in this review doc are now resolved in source.
|
||||
|
||||
## Explicitly Excluded
|
||||
|
||||
- Docstring coverage complaints
|
||||
- PR description / checklist formatting comments
|
||||
- Generic CodeRabbit walkthrough summaries
|
||||
- Duplicate comments that collapse into the same work item
|
||||
@@ -63,6 +63,48 @@
|
||||
"name": "INVALID_TIMESTAMP",
|
||||
"signature": "INVALID_TIMESTAMP()",
|
||||
"selector": "0x118818d1",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryAadhaarImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_TIMESTAMP",
|
||||
"signature": "INVALID_TIMESTAMP()",
|
||||
"selector": "0x118818d1",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryIdCardImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_TIMESTAMP",
|
||||
"signature": "INVALID_TIMESTAMP()",
|
||||
"selector": "0x118818d1",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_TIMESTAMP",
|
||||
"signature": "INVALID_TIMESTAMP()",
|
||||
"selector": "0x118818d1",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryKycImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsCount",
|
||||
"signature": "InvalidRootsCount()",
|
||||
"selector": "0x128781c2",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryAadhaarImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsCount",
|
||||
"signature": "InvalidRootsCount()",
|
||||
"selector": "0x128781c2",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryIdCardImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsCount",
|
||||
"signature": "InvalidRootsCount()",
|
||||
"selector": "0x128781c2",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsCount",
|
||||
"signature": "InvalidRootsCount()",
|
||||
"selector": "0x128781c2",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryKycImplV1.sol"
|
||||
},
|
||||
{
|
||||
@@ -179,6 +221,24 @@
|
||||
"selector": "0x25e62788",
|
||||
"file": "contracts/contracts/libraries/Formatter.sol"
|
||||
},
|
||||
{
|
||||
"name": "ONLY_TEE_CAN_ACCESS",
|
||||
"signature": "ONLY_TEE_CAN_ACCESS()",
|
||||
"selector": "0x2822d0cb",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryAadhaarImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "ONLY_TEE_CAN_ACCESS",
|
||||
"signature": "ONLY_TEE_CAN_ACCESS()",
|
||||
"selector": "0x2822d0cb",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryIdCardImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "ONLY_TEE_CAN_ACCESS",
|
||||
"signature": "ONLY_TEE_CAN_ACCESS()",
|
||||
"selector": "0x2822d0cb",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "ONLY_TEE_CAN_ACCESS",
|
||||
"signature": "ONLY_TEE_CAN_ACCESS()",
|
||||
@@ -197,6 +257,30 @@
|
||||
"selector": "0x29393238",
|
||||
"file": "contracts/contracts/tests/TestAirdrop.sol"
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsHash",
|
||||
"signature": "InvalidRootsHash()",
|
||||
"selector": "0x372c4a4b",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryAadhaarImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsHash",
|
||||
"signature": "InvalidRootsHash()",
|
||||
"selector": "0x372c4a4b",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryIdCardImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsHash",
|
||||
"signature": "InvalidRootsHash()",
|
||||
"selector": "0x372c4a4b",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "InvalidRootsHash",
|
||||
"signature": "InvalidRootsHash()",
|
||||
"selector": "0x372c4a4b",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryKycImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "InvalidFieldElement",
|
||||
"signature": "InvalidFieldElement()",
|
||||
@@ -377,6 +461,24 @@
|
||||
"selector": "0x6f26ab8d",
|
||||
"file": "contracts/contracts/libraries/RegisterProofVerifierLib.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_PROOF",
|
||||
"signature": "INVALID_PROOF()",
|
||||
"selector": "0x712eb087",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryAadhaarImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_PROOF",
|
||||
"signature": "INVALID_PROOF()",
|
||||
"selector": "0x712eb087",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryIdCardImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_PROOF",
|
||||
"signature": "INVALID_PROOF()",
|
||||
"selector": "0x712eb087",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_PROOF",
|
||||
"signature": "INVALID_PROOF()",
|
||||
@@ -389,6 +491,24 @@
|
||||
"selector": "0x71b125ed",
|
||||
"file": "contracts/contracts/IdentityVerificationHubImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_IMAGE",
|
||||
"signature": "INVALID_IMAGE()",
|
||||
"selector": "0x7f91b413",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryAadhaarImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_IMAGE",
|
||||
"signature": "INVALID_IMAGE()",
|
||||
"selector": "0x7f91b413",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryIdCardImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_IMAGE",
|
||||
"signature": "INVALID_IMAGE()",
|
||||
"selector": "0x7f91b413",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_IMAGE",
|
||||
"signature": "INVALID_IMAGE()",
|
||||
@@ -623,6 +743,24 @@
|
||||
"selector": "0xed8cf9ff",
|
||||
"file": "contracts/contracts/IdentityVerificationHubImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_ROOT_CA",
|
||||
"signature": "INVALID_ROOT_CA()",
|
||||
"selector": "0xee57533e",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryAadhaarImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_ROOT_CA",
|
||||
"signature": "INVALID_ROOT_CA()",
|
||||
"selector": "0xee57533e",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryIdCardImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_ROOT_CA",
|
||||
"signature": "INVALID_ROOT_CA()",
|
||||
"selector": "0xee57533e",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "INVALID_ROOT_CA",
|
||||
"signature": "INVALID_ROOT_CA()",
|
||||
@@ -671,6 +809,24 @@
|
||||
"selector": "0xf53393a7",
|
||||
"file": "contracts/contracts/libraries/RootCheckLib.sol"
|
||||
},
|
||||
{
|
||||
"name": "TEE_NOT_SET",
|
||||
"signature": "TEE_NOT_SET()",
|
||||
"selector": "0xfc833fc6",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryAadhaarImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "TEE_NOT_SET",
|
||||
"signature": "TEE_NOT_SET()",
|
||||
"selector": "0xfc833fc6",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryIdCardImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "TEE_NOT_SET",
|
||||
"signature": "TEE_NOT_SET()",
|
||||
"selector": "0xfc833fc6",
|
||||
"file": "contracts/contracts/registry/IdentityRegistryImplV1.sol"
|
||||
},
|
||||
{
|
||||
"name": "TEE_NOT_SET",
|
||||
"signature": "TEE_NOT_SET()",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"0x0b42b970": "InvalidPubSignalsLength",
|
||||
"0x0ee78d58": "NoVerifierSet",
|
||||
"0x118818d1": "INVALID_TIMESTAMP",
|
||||
"0x128781c2": "InvalidRootsCount",
|
||||
"0x12ec75fe": "InvalidAttestationId",
|
||||
"0x153745d3": "RegistrationNotOpen",
|
||||
"0x1644e049": "InvalidDscProof",
|
||||
@@ -15,6 +16,7 @@
|
||||
"0x25e62788": "InvalidMonthRange",
|
||||
"0x2822d0cb": "ONLY_TEE_CAN_ACCESS",
|
||||
"0x29393238": "UserIdentifierAlreadyRegistered",
|
||||
"0x372c4a4b": "InvalidRootsHash",
|
||||
"0x3ae4ed6b": "InvalidFieldElement",
|
||||
"0x422cc3b7": "InvalidPubkey",
|
||||
"0x49aecbc2": "InvalidOlderThan",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"lint:headers": "node scripts/check-duplicate-headers.cjs . && node scripts/check-license-headers.mjs . --check",
|
||||
"lint:headers:fix": "node scripts/check-duplicate-headers.cjs . && node scripts/check-license-headers.mjs . --fix",
|
||||
"prepare": "husky",
|
||||
"reinstall-app": "yarn install && (cd app && yarn clean:watchman && yarn clean:build && yarn clean:ios && yarn clean:xcode && yarn clean:pod-cache && yarn clean:android-deps && rm -rf app/node_modules) && yarn install && yarn workspace @selfxyz/mobile-app run install-app",
|
||||
"reinstall-app": "yarn install && (cd app && yarn clean:watchman && yarn clean:build && yarn clean:ios && yarn clean:xcode && yarn clean:pod-cache && yarn clean:android-deps && yarn clean:ruby && yarn clean:node) && yarn install && yarn workspace @selfxyz/mobile-app run install-app",
|
||||
"sort-package-jsons": "find . -name 'package.json' -not -path './node_modules/*' -not -path './*/node_modules/*' | xargs npx sort-package-json",
|
||||
"test": "yarn workspaces foreach --parallel -i --all run test",
|
||||
"test:license-headers": "cd scripts/tests && node check-license-headers.test.mjs",
|
||||
@@ -67,7 +67,7 @@
|
||||
"punycode": "npm:punycode.js@^2.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-native-passkey": "3.3.2",
|
||||
"react-native-passkey": "^3.3.3",
|
||||
"react-native-webview": "13.16.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -74,6 +74,12 @@ android {
|
||||
.toInt()
|
||||
versionCode = 1
|
||||
versionName = "1.0.0"
|
||||
|
||||
buildConfigField("String", "WEBVIEW_DEV_URL", "\"${System.getenv("WEBVIEW_DEV_URL") ?: ""}\"")
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
package xyz.self.testapp.screens
|
||||
|
||||
import xyz.self.testapp.BuildConfig
|
||||
|
||||
actual fun getDevServerUrl(): String? = BuildConfig.WEBVIEW_DEV_URL.ifBlank { null }
|
||||
@@ -48,6 +48,8 @@ import xyz.self.sdk.api.SelfSdkError
|
||||
import xyz.self.sdk.api.VerificationRequest
|
||||
import xyz.self.sdk.api.VerificationResult
|
||||
|
||||
expect fun getDevServerUrl(): String?
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SdkLaunchScreen(navController: NavController) {
|
||||
@@ -65,14 +67,17 @@ fun SdkLaunchScreen(navController: NavController) {
|
||||
|
||||
val environment = if (useMockDocument) SelfEnvironment.STG else SelfEnvironment.PROD
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val devServerUrl = remember { getDevServerUrl() }
|
||||
val sdk =
|
||||
remember(environment, appName, appEndpoint) {
|
||||
SelfSdk.configure(
|
||||
SelfSdkConfig(
|
||||
environment = environment,
|
||||
debug = true,
|
||||
version = if (useMockDocument) 1 else 2,
|
||||
appName = appName.ifBlank { null },
|
||||
appEndpoint = appEndpoint.ifBlank { null },
|
||||
devServerUrl = devServerUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
package xyz.self.testapp.screens
|
||||
|
||||
import platform.Foundation.NSBundle
|
||||
|
||||
actual fun getDevServerUrl(): String? {
|
||||
val value = NSBundle.mainBundle.objectForInfoDictionaryKey("WEBVIEW_DEV_URL") as? String
|
||||
if (value.isNullOrBlank() || value.startsWith("$(")) return null
|
||||
return value
|
||||
}
|
||||
@@ -18,6 +18,8 @@
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>WEBVIEW_DEV_URL</key>
|
||||
<string>$(WEBVIEW_DEV_URL)</string>
|
||||
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
|
||||
<array>
|
||||
<string>A0000002471001</string>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "./scripts/run-android.sh",
|
||||
"ios": "./scripts/run-ios.sh",
|
||||
"android:build": "./gradlew :composeApp:assembleDebug",
|
||||
"clean": "./gradlew clean",
|
||||
"format": "./gradlew ktlintFormat && cd iosApp && swiftlint --fix --format",
|
||||
|
||||
84
packages/kmp-sdk-test-app/scripts/run-ios.sh
Executable file
84
packages/kmp-sdk-test-app/scripts/run-ios.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
IOS_DIR="$SCRIPT_DIR/iosApp"
|
||||
KMP_SDK_DIR="$SCRIPT_DIR/../kmp-sdk"
|
||||
|
||||
# --- Build KMP framework for iOS Simulator ---
|
||||
echo "🔨 Building KMP framework for iOS Simulator..."
|
||||
cd "$KMP_SDK_DIR"
|
||||
./gradlew :shared:linkDebugFrameworkIosSimulatorArm64
|
||||
|
||||
# --- Resolve Xcode project dependencies ---
|
||||
cd "$IOS_DIR"
|
||||
echo "📦 Resolving package dependencies..."
|
||||
xcodebuild -workspace iosApp.xcworkspace -resolvePackageDependencies -quiet 2>/dev/null || true
|
||||
|
||||
# --- Find an available iOS Simulator ---
|
||||
echo "📱 Finding iOS Simulator..."
|
||||
SIMULATOR_ID=$(xcrun simctl list devices available --json | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for runtime, devices in data['devices'].items():
|
||||
if 'iOS' not in runtime:
|
||||
continue
|
||||
for d in devices:
|
||||
if d['isAvailable'] and 'iPhone' in d['name']:
|
||||
print(d['udid'])
|
||||
sys.exit(0)
|
||||
print('')
|
||||
")
|
||||
|
||||
if [ -z "$SIMULATOR_ID" ]; then
|
||||
echo "❌ No available iPhone simulator found."
|
||||
echo " Open Xcode > Settings > Platforms to install an iOS Simulator runtime."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SIMULATOR_NAME=$(xcrun simctl list devices available | grep "$SIMULATOR_ID" | sed 's/(.*//' | xargs)
|
||||
echo "✅ Using simulator: $SIMULATOR_NAME ($SIMULATOR_ID)"
|
||||
|
||||
# --- Boot simulator if not already booted ---
|
||||
BOOT_STATE=$(xcrun simctl list devices | grep "$SIMULATOR_ID" | grep -o "(Booted)" || true)
|
||||
if [ -z "$BOOT_STATE" ]; then
|
||||
echo "🚀 Booting simulator..."
|
||||
xcrun simctl boot "$SIMULATOR_ID" 2>/dev/null || true
|
||||
open -a Simulator --args -CurrentDeviceUDID "$SIMULATOR_ID"
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
# --- Build the app ---
|
||||
echo "🔨 Building iOS app..."
|
||||
xcodebuild -workspace iosApp.xcworkspace \
|
||||
-scheme iosApp \
|
||||
-sdk iphonesimulator \
|
||||
-destination "id=$SIMULATOR_ID" \
|
||||
ONLY_ACTIVE_ARCH=YES \
|
||||
ARCHS=arm64 \
|
||||
WEBVIEW_DEV_URL="${WEBVIEW_DEV_URL:-}" \
|
||||
build \
|
||||
2>&1 | tail -5
|
||||
|
||||
# --- Find and install the app ---
|
||||
BUILD_DIR=$(xcodebuild -workspace iosApp.xcworkspace \
|
||||
-scheme iosApp \
|
||||
-sdk iphonesimulator \
|
||||
-showBuildSettings -json 2>/dev/null \
|
||||
| jq -r '.[] | select(.target == "iosApp") | .buildSettings.BUILT_PRODUCTS_DIR')
|
||||
APP_PATH="$BUILD_DIR/iosApp.app"
|
||||
|
||||
if [ ! -d "$APP_PATH" ]; then
|
||||
echo "❌ Build output not found at $APP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Installing app on simulator..."
|
||||
xcrun simctl install "$SIMULATOR_ID" "$APP_PATH"
|
||||
|
||||
# --- Launch the app ---
|
||||
BUNDLE_ID=$(defaults read "$APP_PATH/Info.plist" CFBundleIdentifier 2>/dev/null || echo "xyz.self.testapp")
|
||||
echo "🚀 Launching $BUNDLE_ID..."
|
||||
xcrun simctl launch "$SIMULATOR_ID" "$BUNDLE_ID"
|
||||
|
||||
echo "✅ Done!"
|
||||
@@ -124,6 +124,9 @@ actual class SelfSdk private constructor(
|
||||
putExtra(SelfVerificationActivity.EXTRA_DEBUG_MODE, config.debug)
|
||||
putExtra(SelfVerificationActivity.EXTRA_VERIFICATION_REQUEST, serializeRequest(request))
|
||||
putExtra(SelfVerificationActivity.EXTRA_CONFIG, serializeConfig(config))
|
||||
if (config.devServerUrl != null) {
|
||||
putExtra(SelfVerificationActivity.EXTRA_DEV_SERVER_URL, config.devServerUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// Launch the verification activity
|
||||
|
||||
@@ -11,7 +11,6 @@ import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.net.http.SslError
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.PermissionRequest
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.ValueCallback
|
||||
@@ -21,12 +20,17 @@ import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.webkit.WebMessageCompat
|
||||
import androidx.webkit.WebViewCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import xyz.self.sdk.bridge.MessageRouter
|
||||
|
||||
class AndroidWebViewHost(
|
||||
private val context: Context,
|
||||
private val router: MessageRouter,
|
||||
private val isDebugMode: Boolean = false,
|
||||
private val remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app",
|
||||
private val devServerUrl: String? = null,
|
||||
) {
|
||||
private lateinit var webView: WebView
|
||||
var pendingPermissionRequest: PermissionRequest? = null
|
||||
@@ -53,13 +57,7 @@ class AndroidWebViewHost(
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): Boolean {
|
||||
val uri = request?.url ?: return true
|
||||
val isAllowed =
|
||||
(uri.scheme == "https" && uri.host == "self-app-alpha.vercel.app") ||
|
||||
(isDebugMode && uri.scheme == "http" && uri.host == "127.0.0.1" && uri.port == 5173)
|
||||
return !isAllowed
|
||||
}
|
||||
): Boolean = !isAllowedNavigationUrl(request?.url?.toString(), isDebugMode, remoteWebAppBaseUrl, devServerUrl)
|
||||
|
||||
override fun onReceivedSslError(
|
||||
view: WebView?,
|
||||
@@ -79,11 +77,7 @@ class AndroidWebViewHost(
|
||||
request.deny()
|
||||
return
|
||||
}
|
||||
val isTrusted =
|
||||
(origin.scheme == "https" && origin.host == "self-app-alpha.vercel.app") ||
|
||||
(origin.scheme == "https" && origin.host == "verify.didit.me") ||
|
||||
(isDebugMode && origin.scheme == "http" && origin.host == "127.0.0.1")
|
||||
if (!isTrusted) {
|
||||
if (!isTrustedPermissionOrigin(origin.toString(), isDebugMode, remoteWebAppBaseUrl, devServerUrl)) {
|
||||
request.deny()
|
||||
return
|
||||
}
|
||||
@@ -154,11 +148,9 @@ class AndroidWebViewHost(
|
||||
}
|
||||
}
|
||||
|
||||
addJavascriptInterface(BridgeJsInterface(), "SelfNativeAndroid")
|
||||
installBridge(webView = this)
|
||||
|
||||
val baseUrl = "https://self-app-alpha.vercel.app/tunnel/tour/1"
|
||||
val url = if (queryParams.isNotEmpty()) "$baseUrl?$queryParams" else baseUrl
|
||||
loadUrl(url)
|
||||
loadUrl(initialContentUrl(queryParams, isDebugMode, remoteWebAppBaseUrl, devServerUrl))
|
||||
}
|
||||
return webView
|
||||
}
|
||||
@@ -173,15 +165,172 @@ class AndroidWebViewHost(
|
||||
webView.destroy()
|
||||
}
|
||||
|
||||
inner class BridgeJsInterface {
|
||||
@JavascriptInterface
|
||||
fun postMessage(json: String) {
|
||||
router.onMessageReceived(json)
|
||||
private fun installBridge(webView: WebView) {
|
||||
check(WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
|
||||
"WEB_MESSAGE_LISTENER not supported — native bridge unavailable on this device"
|
||||
}
|
||||
|
||||
WebViewCompat.addWebMessageListener(
|
||||
webView,
|
||||
"SelfNativeAndroid",
|
||||
buildAllowedOriginRules(isDebugMode, remoteWebAppBaseUrl, devServerUrl),
|
||||
) { _, message: WebMessageCompat, sourceOrigin, isMainFrame, _ ->
|
||||
if (!isMainFrame) {
|
||||
return@addWebMessageListener
|
||||
}
|
||||
|
||||
val rawJson = message.data ?: return@addWebMessageListener
|
||||
router.onMessageReceived(
|
||||
rawJson = rawJson,
|
||||
isTrustedSource = isTrustedBridgeOrigin(sourceOrigin.toString(), isDebugMode, remoteWebAppBaseUrl, devServerUrl),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FILE_CHOOSER_REQUEST_CODE = 1001
|
||||
const val CAMERA_PERMISSION_REQUEST_CODE = 1002
|
||||
private const val BUNDLED_TOUR_PATH = "/tunnel/tour/1"
|
||||
private const val DEBUG_HOST = "127.0.0.1"
|
||||
private const val DEBUG_PORT = 5173
|
||||
private const val DIDIT_HOST = "verify.didit.me"
|
||||
|
||||
internal fun initialContentUrl(
|
||||
queryParams: String,
|
||||
isDebugMode: Boolean,
|
||||
remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app",
|
||||
devServerUrl: String? = null,
|
||||
): String {
|
||||
val baseUrl =
|
||||
when {
|
||||
isDebugMode && devServerUrl != null -> devServerUrl.trimEnd('/')
|
||||
isDebugMode -> "http://$DEBUG_HOST:$DEBUG_PORT"
|
||||
else -> {
|
||||
require(remoteWebAppBaseUrl.startsWith("https://")) {
|
||||
"remoteWebAppBaseUrl must use HTTPS in release builds"
|
||||
}
|
||||
remoteWebAppBaseUrl.trimEnd('/')
|
||||
}
|
||||
}
|
||||
return buildString {
|
||||
append(baseUrl).append(BUNDLED_TOUR_PATH)
|
||||
if (queryParams.isNotEmpty()) {
|
||||
append("?").append(queryParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun isAllowedNavigationUrl(
|
||||
rawUrl: String?,
|
||||
isDebugMode: Boolean,
|
||||
remoteWebAppBaseUrl: String? = null,
|
||||
devServerUrl: String? = null,
|
||||
): Boolean =
|
||||
isRemoteOrigin(rawUrl, remoteWebAppBaseUrl) ||
|
||||
isDiditUrl(rawUrl) ||
|
||||
(isDebugMode && isDebugLocalUrl(rawUrl)) ||
|
||||
(isDebugMode && isDevServerUrl(rawUrl, devServerUrl))
|
||||
|
||||
internal fun isTrustedPermissionOrigin(
|
||||
rawUrl: String?,
|
||||
isDebugMode: Boolean,
|
||||
remoteWebAppBaseUrl: String? = null,
|
||||
devServerUrl: String? = null,
|
||||
): Boolean =
|
||||
isRemoteOrigin(rawUrl, remoteWebAppBaseUrl) ||
|
||||
isDiditUrl(rawUrl) ||
|
||||
(isDebugMode && isDebugLocalUrl(rawUrl)) ||
|
||||
(isDebugMode && isDevServerUrl(rawUrl, devServerUrl))
|
||||
|
||||
internal fun isTrustedBridgeOrigin(
|
||||
rawUrl: String?,
|
||||
isDebugMode: Boolean,
|
||||
remoteWebAppBaseUrl: String? = null,
|
||||
devServerUrl: String? = null,
|
||||
): Boolean =
|
||||
isRemoteOrigin(rawUrl, remoteWebAppBaseUrl) ||
|
||||
(isDebugMode && isDebugLocalUrl(rawUrl)) ||
|
||||
(isDebugMode && isDevServerUrl(rawUrl, devServerUrl))
|
||||
|
||||
internal fun isRemoteOrigin(
|
||||
rawUrl: String?,
|
||||
remoteWebAppBaseUrl: String?,
|
||||
): Boolean {
|
||||
if (rawUrl == null || remoteWebAppBaseUrl == null) return false
|
||||
val url = parseUri(rawUrl) ?: return false
|
||||
val remote = parseUri(remoteWebAppBaseUrl) ?: return false
|
||||
return url.scheme == remote.scheme &&
|
||||
(url.host ?: url.authority) == (remote.host ?: remote.authority) &&
|
||||
resolvedPort(url) == resolvedPort(remote)
|
||||
}
|
||||
|
||||
private fun isDiditUrl(rawUrl: String?): Boolean {
|
||||
val port = uriPort(rawUrl)
|
||||
return uriScheme(rawUrl) == "https" &&
|
||||
uriHost(rawUrl) == DIDIT_HOST &&
|
||||
(port == null || port == 443)
|
||||
}
|
||||
|
||||
private fun isDebugLocalUrl(rawUrl: String?): Boolean =
|
||||
uriScheme(rawUrl) == "http" && uriHost(rawUrl) == DEBUG_HOST && uriPort(rawUrl) == DEBUG_PORT
|
||||
|
||||
private fun isDevServerUrl(
|
||||
rawUrl: String?,
|
||||
devServerUrl: String?,
|
||||
): Boolean {
|
||||
if (rawUrl == null || devServerUrl == null) return false
|
||||
val url = parseUri(rawUrl) ?: return false
|
||||
val dev = parseUri(devServerUrl) ?: return false
|
||||
return url.scheme == dev.scheme &&
|
||||
(url.host ?: url.authority) == (dev.host ?: dev.authority) &&
|
||||
resolvedPort(url) == resolvedPort(dev)
|
||||
}
|
||||
|
||||
private fun buildAllowedOriginRules(
|
||||
isDebugMode: Boolean,
|
||||
remoteWebAppBaseUrl: String,
|
||||
devServerUrl: String? = null,
|
||||
): Set<String> {
|
||||
val remote = parseUri(remoteWebAppBaseUrl)
|
||||
return buildSet {
|
||||
if (remote != null && remote.scheme == "https") {
|
||||
val host = remote.host ?: remote.authority
|
||||
val port = resolvedPort(remote)
|
||||
val defaultPort = if (remote.scheme == "https") 443 else 80
|
||||
if (port != defaultPort) {
|
||||
add("${remote.scheme}://$host:$port")
|
||||
} else {
|
||||
add("${remote.scheme}://$host")
|
||||
}
|
||||
}
|
||||
if (isDebugMode) {
|
||||
add("http://$DEBUG_HOST:$DEBUG_PORT")
|
||||
devServerUrl?.let { parseUri(it) }?.let { dev ->
|
||||
val host = dev.host ?: dev.authority
|
||||
val port = resolvedPort(dev)
|
||||
val defaultPort = if (dev.scheme == "https") 443 else 80
|
||||
if (port != defaultPort) {
|
||||
add("${dev.scheme}://$host:$port")
|
||||
} else {
|
||||
add("${dev.scheme}://$host")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolvedPort(uri: java.net.URI): Int {
|
||||
val port = uri.port
|
||||
if (port != -1) return port
|
||||
return if (uri.scheme == "https") 443 else 80
|
||||
}
|
||||
|
||||
private fun uriScheme(rawUrl: String?): String? = parseUri(rawUrl)?.scheme
|
||||
|
||||
private fun uriHost(rawUrl: String?): String? = parseUri(rawUrl)?.host ?: parseUri(rawUrl)?.authority
|
||||
|
||||
private fun uriPort(rawUrl: String?): Int? = parseUri(rawUrl)?.port?.takeIf { it != -1 }
|
||||
|
||||
private fun parseUri(rawUrl: String?): java.net.URI? = rawUrl?.let { raw -> runCatching { java.net.URI(raw) }.getOrNull() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,13 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebChromeClient
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import xyz.self.sdk.bridge.MessageRouter
|
||||
import xyz.self.sdk.handlers.CryptoBridgeHandler
|
||||
import xyz.self.sdk.handlers.LifecycleBridgeHandler
|
||||
@@ -21,9 +26,11 @@ import xyz.self.sdk.providers.SdkProviderRegistry
|
||||
class SelfVerificationActivity : AppCompatActivity() {
|
||||
private lateinit var webViewHost: AndroidWebViewHost
|
||||
private lateinit var router: MessageRouter
|
||||
private var container: FrameLayout? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
initVerificationFlow()
|
||||
}
|
||||
|
||||
@@ -63,9 +70,43 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
return
|
||||
}
|
||||
|
||||
webViewHost = AndroidWebViewHost(this, router, isDebugMode)
|
||||
val configJson = intent.getStringExtra(EXTRA_CONFIG) ?: "{}"
|
||||
val remoteWebAppBaseUrl =
|
||||
try {
|
||||
org.json.JSONObject(configJson).optString("remoteWebAppBaseUrl", "https://self-app-alpha.vercel.app")
|
||||
} catch (_: Exception) {
|
||||
"https://self-app-alpha.vercel.app"
|
||||
}
|
||||
|
||||
val devServerUrl = intent.getStringExtra(EXTRA_DEV_SERVER_URL)
|
||||
webViewHost = AndroidWebViewHost(this, router, isDebugMode, remoteWebAppBaseUrl, devServerUrl)
|
||||
val webView = webViewHost.createWebView(queryParams)
|
||||
setContentView(webView)
|
||||
val wrapper =
|
||||
FrameLayout(this).apply {
|
||||
addView(
|
||||
webView,
|
||||
ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
),
|
||||
)
|
||||
}
|
||||
this.container = wrapper
|
||||
setContentView(wrapper)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(wrapper) { view, insets ->
|
||||
val systemInsets =
|
||||
insets.getInsets(
|
||||
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(),
|
||||
)
|
||||
view.setPadding(
|
||||
systemInsets.left,
|
||||
systemInsets.top,
|
||||
systemInsets.right,
|
||||
systemInsets.bottom,
|
||||
)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerHandlers() {
|
||||
@@ -169,6 +210,7 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
container?.let { ViewCompat.setOnApplyWindowInsetsListener(it, null) }
|
||||
if (::webViewHost.isInitialized) {
|
||||
webViewHost.destroy()
|
||||
}
|
||||
@@ -179,6 +221,7 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
const val EXTRA_DEBUG_MODE = "xyz.self.sdk.DEBUG_MODE"
|
||||
const val EXTRA_VERIFICATION_REQUEST = "xyz.self.sdk.VERIFICATION_REQUEST"
|
||||
const val EXTRA_CONFIG = "xyz.self.sdk.CONFIG"
|
||||
const val EXTRA_DEV_SERVER_URL = "xyz.self.sdk.DEV_SERVER_URL"
|
||||
|
||||
const val RESULT_CODE_SUCCESS = RESULT_OK
|
||||
const val RESULT_CODE_ERROR = RESULT_FIRST_USER
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
package xyz.self.sdk.webview
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class AndroidWebViewHostSecurityTest {
|
||||
private val remoteUrl = "https://self-app-alpha.vercel.app"
|
||||
|
||||
@Test
|
||||
fun `release builds launch remote content`() {
|
||||
assertEquals(
|
||||
"https://self-app-alpha.vercel.app/tunnel/tour/1",
|
||||
AndroidWebViewHost.initialContentUrl(queryParams = "", isDebugMode = false, remoteWebAppBaseUrl = remoteUrl),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `debug builds launch localhost content`() {
|
||||
assertEquals(
|
||||
"http://127.0.0.1:5173/tunnel/tour/1",
|
||||
AndroidWebViewHost.initialContentUrl(queryParams = "", isDebugMode = true, remoteWebAppBaseUrl = remoteUrl),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `navigation allows remote origin and didit`() {
|
||||
assertTrue(
|
||||
AndroidWebViewHost.isAllowedNavigationUrl(
|
||||
"https://self-app-alpha.vercel.app/tunnel/tour/1",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteUrl,
|
||||
),
|
||||
)
|
||||
assertTrue(
|
||||
AndroidWebViewHost.isAllowedNavigationUrl(
|
||||
"https://verify.didit.me/session/123",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteUrl,
|
||||
),
|
||||
)
|
||||
assertTrue(
|
||||
AndroidWebViewHost.isAllowedNavigationUrl(
|
||||
"http://127.0.0.1:5173/tunnel/tour/1",
|
||||
isDebugMode = true,
|
||||
remoteWebAppBaseUrl = remoteUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `navigation rejects arbitrary origins`() {
|
||||
assertFalse(
|
||||
AndroidWebViewHost.isAllowedNavigationUrl(
|
||||
"https://evil.com/tunnel/tour/1",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteUrl,
|
||||
),
|
||||
)
|
||||
assertFalse(
|
||||
AndroidWebViewHost.isAllowedNavigationUrl(
|
||||
"http://example.com/test",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `release build rejects HTTP base URL`() {
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
AndroidWebViewHost.initialContentUrl(
|
||||
queryParams = "",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = "http://self-app-alpha.vercel.app",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `didit on non-443 port is rejected`() {
|
||||
assertFalse(
|
||||
AndroidWebViewHost.isAllowedNavigationUrl(
|
||||
"https://verify.didit.me:8443/session/123",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bridge trust is limited to remote origin in release`() {
|
||||
assertTrue(
|
||||
AndroidWebViewHost.isTrustedBridgeOrigin(
|
||||
"https://self-app-alpha.vercel.app/tunnel/tour/1",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteUrl,
|
||||
),
|
||||
)
|
||||
assertFalse(
|
||||
AndroidWebViewHost.isTrustedBridgeOrigin(
|
||||
"https://verify.didit.me/session/123",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteUrl,
|
||||
),
|
||||
)
|
||||
assertFalse(
|
||||
AndroidWebViewHost.isTrustedBridgeOrigin(
|
||||
"https://evil.com/tunnel/tour/1",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -34,4 +34,6 @@ data class SelfSdkConfig(
|
||||
val appEndpoint: String? = null,
|
||||
val endpointType: String? = null,
|
||||
val chainID: Int? = null,
|
||||
val remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app",
|
||||
val devServerUrl: String? = null,
|
||||
)
|
||||
|
||||
@@ -27,7 +27,14 @@ class MessageRouter(
|
||||
handlers[handler.domain] = handler
|
||||
}
|
||||
|
||||
fun onMessageReceived(rawJson: String) {
|
||||
fun onMessageReceived(
|
||||
rawJson: String,
|
||||
isTrustedSource: Boolean,
|
||||
) {
|
||||
if (!isTrustedSource) {
|
||||
return // Drop messages from untrusted WebView origins.
|
||||
}
|
||||
|
||||
val request =
|
||||
try {
|
||||
json.decodeFromString<BridgeRequest>(rawJson)
|
||||
|
||||
@@ -44,7 +44,7 @@ class MessageRouterTest {
|
||||
{"type":"request","version":1,"id":"req-1","domain":"haptic","method":"trigger","params":{},"timestamp":123}
|
||||
""".trimIndent()
|
||||
|
||||
router.onMessageReceived(request)
|
||||
router.onMessageReceived(request, isTrustedSource = true)
|
||||
|
||||
assertEquals(1, responses.size)
|
||||
assertTrue(responses[0].contains("_handleResponse"))
|
||||
@@ -62,7 +62,7 @@ class MessageRouterTest {
|
||||
{"type":"request","version":1,"id":"req-1","domain":"haptic","method":"trigger","params":{},"timestamp":123}
|
||||
""".trimIndent()
|
||||
|
||||
router.onMessageReceived(request)
|
||||
router.onMessageReceived(request, isTrustedSource = true)
|
||||
|
||||
assertEquals(1, responses.size)
|
||||
assertTrue(responses[0].contains("DOMAIN_NOT_FOUND"))
|
||||
@@ -95,7 +95,7 @@ class MessageRouterTest {
|
||||
{"type":"request","version":1,"id":"req-2","domain":"crypto","method":"sign","params":{},"timestamp":123}
|
||||
""".trimIndent()
|
||||
|
||||
router.onMessageReceived(request)
|
||||
router.onMessageReceived(request, isTrustedSource = true)
|
||||
|
||||
assertEquals(1, responses.size)
|
||||
assertTrue(responses[0].contains("KEY_NOT_FOUND"))
|
||||
@@ -117,11 +117,32 @@ class MessageRouterTest {
|
||||
val responses = mutableListOf<String>()
|
||||
val router = MessageRouter(sendToWebView = { responses.add(it) })
|
||||
|
||||
router.onMessageReceived("this is not json")
|
||||
router.onMessageReceived("this is not json", isTrustedSource = true)
|
||||
|
||||
assertEquals(0, responses.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun drops_messages_from_untrusted_origins_before_dispatch() =
|
||||
runTest {
|
||||
val responses = mutableListOf<String>()
|
||||
val testScope = TestScope(UnconfinedTestDispatcher(testScheduler))
|
||||
val router =
|
||||
MessageRouter(
|
||||
sendToWebView = { responses.add(it) },
|
||||
scope = testScope,
|
||||
)
|
||||
val handler = FakeBridgeHandler(domain = BridgeDomain.HAPTIC, response = JsonPrimitive("ok"))
|
||||
router.register(handler)
|
||||
|
||||
val untrustedJson =
|
||||
"""{"type":"request","version":1,"id":"req-1","domain":"haptic","method":"trigger","params":{},"timestamp":123}"""
|
||||
router.onMessageReceived(rawJson = untrustedJson, isTrustedSource = false)
|
||||
|
||||
assertEquals(0, responses.size)
|
||||
assertEquals(0, handler.invocations.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pushEvent_sends_handleEvent_to_webview() {
|
||||
val responses = mutableListOf<String>()
|
||||
@@ -160,6 +181,7 @@ class MessageRouterTest {
|
||||
repeat(3) { i ->
|
||||
router.onMessageReceived(
|
||||
"""{"type":"request","version":1,"id":"req-$i","domain":"haptic","method":"trigger","params":{},"timestamp":123}""",
|
||||
isTrustedSource = true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -188,6 +210,7 @@ class MessageRouterTest {
|
||||
|
||||
router.onMessageReceived(
|
||||
"""{"type":"request","version":1,"id":"req-1","domain":"haptic","method":"trigger","params":{},"timestamp":123}""",
|
||||
isTrustedSource = true,
|
||||
)
|
||||
|
||||
assertEquals(1, hapticHandler.invocations.size)
|
||||
@@ -214,6 +237,7 @@ class MessageRouterTest {
|
||||
|
||||
router.onMessageReceived(
|
||||
"""{"type":"request","version":1,"id":"req-1","domain":"nfc","method":"scan","params":{},"timestamp":123}""",
|
||||
isTrustedSource = true,
|
||||
)
|
||||
|
||||
assertEquals(0, handlerA.invocations.size)
|
||||
@@ -235,6 +259,7 @@ class MessageRouterTest {
|
||||
|
||||
router.onMessageReceived(
|
||||
"""{"type":"request","version":1,"id":"my-unique-req-id","domain":"haptic","method":"trigger","params":{},"timestamp":123}""",
|
||||
isTrustedSource = true,
|
||||
)
|
||||
|
||||
assertEquals(1, responses.size)
|
||||
@@ -275,6 +300,7 @@ class MessageRouterTest {
|
||||
|
||||
router.onMessageReceived(
|
||||
"""{"type":"request","version":1,"id":"req-1","domain":"crypto","method":"sign","params":{},"timestamp":123}""",
|
||||
isTrustedSource = true,
|
||||
)
|
||||
|
||||
assertEquals(1, responses.size)
|
||||
|
||||
@@ -100,7 +100,13 @@ actual class SelfSdk private constructor(
|
||||
val queryParams = buildQueryParams(request)
|
||||
|
||||
// Create WebView host and the web view
|
||||
webViewHost = IosWebViewHost(router!!, config.debug)
|
||||
webViewHost =
|
||||
IosWebViewHost(
|
||||
router!!,
|
||||
config.debug,
|
||||
remoteWebAppBaseUrl = config.remoteWebAppBaseUrl,
|
||||
devServerUrl = config.devServerUrl,
|
||||
)
|
||||
webViewHost!!.createWebView(queryParams)
|
||||
|
||||
// Get the ViewController from the WebView provider and present it
|
||||
|
||||
@@ -19,4 +19,10 @@ interface WebViewProvider {
|
||||
fun evaluateJs(js: String)
|
||||
|
||||
fun getViewController(): UIViewController
|
||||
|
||||
fun isBridgeRequestAllowed(): Boolean
|
||||
|
||||
fun configureRemoteLoading(remoteWebAppBaseURL: String?) {}
|
||||
|
||||
fun configureDevServer(devServerUrl: String?) {}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,23 @@ import xyz.self.sdk.providers.IosProviderRegistry
|
||||
class IosWebViewHost(
|
||||
private val router: MessageRouter,
|
||||
private val isDebugMode: Boolean = false,
|
||||
private val remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app",
|
||||
private val devServerUrl: String? = null,
|
||||
) {
|
||||
fun createWebView(queryParams: String? = null): UIView {
|
||||
val provider =
|
||||
IosProviderRegistry.webView
|
||||
?: throw IllegalStateException("WebView provider not configured")
|
||||
|
||||
provider.configureRemoteLoading(remoteWebAppBaseUrl)
|
||||
provider.configureDevServer(devServerUrl)
|
||||
|
||||
return provider.createWebView(
|
||||
onMessageReceived = { rawJson ->
|
||||
router.onMessageReceived(rawJson)
|
||||
router.onMessageReceived(
|
||||
rawJson = rawJson,
|
||||
isTrustedSource = provider.isBridgeRequestAllowed(),
|
||||
)
|
||||
},
|
||||
isDebugMode = isDebugMode,
|
||||
queryParams = queryParams,
|
||||
|
||||
@@ -19,22 +19,23 @@
|
||||
"types": ["vitest/globals", "react"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@selfxyz/common": ["../../common/dist/esm/index"],
|
||||
"@selfxyz/common/constants": ["../../common/dist/esm/src/constants/index"],
|
||||
"@selfxyz/common/constants/countries": ["../../common/dist/esm/src/constants/countries"],
|
||||
"@selfxyz/common/types": ["../../common/dist/esm/src/types/index"],
|
||||
"@selfxyz/common/types/passport": ["../../common/dist/esm/src/types/passport"],
|
||||
"@selfxyz/common/utils": ["../../common/dist/esm/src/utils/index"],
|
||||
"@selfxyz/common/utils/attest": ["../../common/dist/esm/src/utils/attest"],
|
||||
"@selfxyz/common/utils/circuits/registerInputs": ["../../common/dist/esm/src/utils/circuits/registerInputs"],
|
||||
"@selfxyz/common/utils/csca": ["../../common/dist/esm/src/utils/csca"],
|
||||
"@selfxyz/common/utils/hash/sha": ["../../common/dist/esm/src/utils/hash/sha"],
|
||||
"@selfxyz/common/utils/passportFormat": ["../../common/dist/esm/src/utils/passports/format"],
|
||||
"@selfxyz/common/utils/passports": ["../../common/dist/esm/src/utils/passports/index"],
|
||||
"@selfxyz/common/utils/passports/validate": ["../../common/dist/esm/src/utils/passports/validate"],
|
||||
"@selfxyz/common/utils/proving": ["../../common/dist/esm/src/utils/proving"],
|
||||
"@selfxyz/common/utils/types": ["../../common/dist/esm/src/utils/types"]
|
||||
"@selfxyz/common": ["../../common/index.ts"],
|
||||
"@selfxyz/common/constants": ["../../common/src/constants/index.ts"],
|
||||
"@selfxyz/common/constants/countries": ["../../common/src/constants/countries.ts"],
|
||||
"@selfxyz/common/types": ["../../common/src/types/index.ts"],
|
||||
"@selfxyz/common/types/passport": ["../../common/src/types/passport.ts"],
|
||||
"@selfxyz/common/utils": ["../../common/src/utils/index.ts"],
|
||||
"@selfxyz/common/utils/attest": ["../../common/src/utils/attest.ts"],
|
||||
"@selfxyz/common/utils/circuits/registerInputs": ["../../common/src/utils/circuits/registerInputs.ts"],
|
||||
"@selfxyz/common/utils/csca": ["../../common/src/utils/csca.ts"],
|
||||
"@selfxyz/common/utils/hash/sha": ["../../common/src/utils/hash/sha.ts"],
|
||||
"@selfxyz/common/utils/passportFormat": ["../../common/src/utils/passports/format.ts"],
|
||||
"@selfxyz/common/utils/passports": ["../../common/src/utils/passports/index.ts"],
|
||||
"@selfxyz/common/utils/passports/validate": ["../../common/src/utils/passports/validate.ts"],
|
||||
"@selfxyz/common/utils/proving": ["../../common/src/utils/proving.ts"],
|
||||
"@selfxyz/common/utils/types": ["../../common/src/utils/types.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "tests"]
|
||||
"include": ["src", "tests"],
|
||||
"references": [{ "path": "../../common" }]
|
||||
}
|
||||
|
||||
@@ -53,23 +53,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("validateWebViewBundle") {
|
||||
doLast {
|
||||
val bundleDir = file("src/main/assets/self-wallet")
|
||||
val indexFile = file("src/main/assets/self-wallet/index.html")
|
||||
if (!bundleDir.exists() || !indexFile.exists()) {
|
||||
throw GradleException(
|
||||
"WebView bundle not found at src/main/assets/self-wallet/index.html. " +
|
||||
"Run ./scripts/build-webview-bundle.sh from the repo root first.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("preBuild") {
|
||||
dependsOn("validateWebViewBundle")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.webkit:webkit:1.9.0")
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
@font-face{font-family:Advercase;src:url(/fonts/Advercase-Regular.otf) format("opentype");font-weight:400;font-display:swap}@font-face{font-family:DIN OT;src:url(/fonts/DINOT-Medium.otf) format("opentype");font-weight:500;font-display:swap}@font-face{font-family:DIN OT;src:url(/fonts/DINOT-Bold.otf) format("opentype");font-weight:700;font-display:swap}@font-face{font-family:IBM Plex Mono;src:url(/fonts/IBMPlexMono-Regular.otf) format("opentype");font-weight:400;font-display:swap}.launch-recovery-screen{display:flex;flex:1;min-height:0}.launch-recovery-screen img[src$="/backgrounds/restore.png"],.launch-recovery-screen img[src$="restore.png"]{height:auto!important;object-fit:contain!important;object-position:top center!important}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;width:100%;overflow:hidden}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}.tour4-lottie-scale{display:flex;flex-direction:column;height:100%}.tour4-lottie-scale svg{transform:scale(1.35)!important}@keyframes spin{to{transform:rotate(360deg)}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -35,10 +35,7 @@ object SelfSdk {
|
||||
config.chainID?.let { putExtra(SelfVerificationActivity.EXTRA_CHAIN_ID, it) }
|
||||
config.userDefinedData?.let { putExtra(SelfVerificationActivity.EXTRA_USER_DEFINED_DATA, it) }
|
||||
config.selfDefinedData?.let { putExtra(SelfVerificationActivity.EXTRA_SELF_DEFINED_DATA, it) }
|
||||
config.remoteWebAppBaseUrl?.let { putExtra(SelfVerificationActivity.EXTRA_REMOTE_WEB_APP_BASE_URL, it) }
|
||||
config.remoteWebAppIntegritySha256?.let {
|
||||
putExtra(SelfVerificationActivity.EXTRA_REMOTE_WEB_APP_INTEGRITY_SHA256, it)
|
||||
}
|
||||
putExtra(SelfVerificationActivity.EXTRA_REMOTE_WEB_APP_BASE_URL, config.remoteWebAppBaseUrl)
|
||||
}
|
||||
activity.startActivityForResult(intent, requestCode)
|
||||
}
|
||||
|
||||
@@ -19,8 +19,7 @@ data class SelfSdkConfig(
|
||||
val chainID: Int? = null,
|
||||
val userDefinedData: String? = null,
|
||||
val selfDefinedData: String? = null,
|
||||
val remoteWebAppBaseUrl: String? = null,
|
||||
val remoteWebAppIntegritySha256: String? = null,
|
||||
val remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app",
|
||||
)
|
||||
|
||||
class SelfSdkException(
|
||||
|
||||
@@ -26,7 +26,15 @@ class MessageRouter(
|
||||
handlers[handler.domain] = handler
|
||||
}
|
||||
|
||||
fun onMessageReceived(rawJson: String) {
|
||||
fun onMessageReceived(
|
||||
rawJson: String,
|
||||
isTrustedSource: Boolean,
|
||||
) {
|
||||
if (!isTrustedSource) {
|
||||
android.util.Log.w("BridgeRouter", "Dropped message from untrusted WebView origin")
|
||||
return
|
||||
}
|
||||
|
||||
val request =
|
||||
try {
|
||||
json.decodeFromString<BridgeRequest>(rawJson)
|
||||
|
||||
@@ -10,29 +10,26 @@ import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.net.http.SslError
|
||||
import android.util.Log
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.PermissionRequest
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.ValueCallback
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
import androidx.webkit.WebMessageCompat
|
||||
import androidx.webkit.WebViewCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import xyz.self.sdk.bridge.MessageRouter
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
|
||||
class AndroidWebViewHost(
|
||||
private val context: Context,
|
||||
private val router: MessageRouter,
|
||||
private val isDebugMode: Boolean = false,
|
||||
private val remoteWebAppBaseUrl: String? = null,
|
||||
private val remoteWebAppIntegritySha256: String? = null,
|
||||
private val remoteWebAppBaseUrl: String = DEFAULT_REMOTE_BASE_URL,
|
||||
) {
|
||||
private lateinit var webView: WebView
|
||||
|
||||
@@ -44,42 +41,6 @@ class AndroidWebViewHost(
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
fun createWebView(queryParams: String): WebView {
|
||||
isDestroyed = false
|
||||
val selfWalletHandler =
|
||||
WebViewAssetLoader.PathHandler { rawPath ->
|
||||
try {
|
||||
val normalizedPath = rawPath.removePrefix("/")
|
||||
val assetPath =
|
||||
if (normalizedPath.isEmpty() || !normalizedPath.contains('.')) {
|
||||
"self-wallet/index.html"
|
||||
} else {
|
||||
"self-wallet/$normalizedPath"
|
||||
}
|
||||
val inputStream = context.assets.open(assetPath)
|
||||
val mimeType =
|
||||
when {
|
||||
assetPath.endsWith(".js") -> "application/javascript"
|
||||
assetPath.endsWith(".css") -> "text/css"
|
||||
assetPath.endsWith(".html") -> "text/html"
|
||||
assetPath.endsWith(".json") -> "application/json"
|
||||
assetPath.endsWith(".woff2") -> "font/woff2"
|
||||
assetPath.endsWith(".woff") -> "font/woff"
|
||||
assetPath.endsWith(".otf") -> "font/otf"
|
||||
assetPath.endsWith(".ttf") -> "font/ttf"
|
||||
assetPath.endsWith(".png") -> "image/png"
|
||||
assetPath.endsWith(".svg") -> "image/svg+xml"
|
||||
else -> "application/octet-stream"
|
||||
}
|
||||
WebResourceResponse(mimeType, "UTF-8", inputStream)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val assetLoader =
|
||||
WebViewAssetLoader
|
||||
.Builder()
|
||||
.addPathHandler("/", selfWalletHandler)
|
||||
.build()
|
||||
|
||||
webView =
|
||||
WebView(context).apply {
|
||||
@@ -97,26 +58,15 @@ class AndroidWebViewHost(
|
||||
|
||||
webViewClient =
|
||||
object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): WebResourceResponse? {
|
||||
request ?: return null
|
||||
val url = request.url
|
||||
if (url.host != BUNDLED_HOST) return null
|
||||
return assetLoader.shouldInterceptRequest(url)
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): Boolean {
|
||||
val url = request?.url ?: return true
|
||||
if (isBundledOrigin(url)) return false
|
||||
if (isDebugMode && isDebugOrigin(url)) return false
|
||||
if (isAllowedRemoteOrigin(url.toString())) return false
|
||||
return true
|
||||
}
|
||||
): Boolean =
|
||||
!isAllowedNavigationUrl(
|
||||
request?.url?.toString(),
|
||||
isDebugMode,
|
||||
remoteWebAppBaseUrl,
|
||||
)
|
||||
|
||||
override fun onReceivedSslError(
|
||||
view: WebView?,
|
||||
@@ -132,14 +82,13 @@ class AndroidWebViewHost(
|
||||
override fun onPermissionRequest(request: PermissionRequest?) {
|
||||
request ?: return
|
||||
|
||||
val originStr = request.origin?.toString() ?: ""
|
||||
val originUri = Uri.parse(originStr)
|
||||
val isTrusted =
|
||||
isBundledOrigin(originUri) ||
|
||||
(isDebugMode && isDebugOrigin(originUri)) ||
|
||||
isMatchingOrigin(originUri, "https", "verify.didit.me", 443) ||
|
||||
isAllowedRemoteOrigin(originStr)
|
||||
if (!isTrusted) {
|
||||
if (
|
||||
!isTrustedPermissionOrigin(
|
||||
request.origin?.toString(),
|
||||
isDebugMode,
|
||||
remoteWebAppBaseUrl,
|
||||
)
|
||||
) {
|
||||
request.deny()
|
||||
return
|
||||
}
|
||||
@@ -150,11 +99,20 @@ class AndroidWebViewHost(
|
||||
return
|
||||
}
|
||||
|
||||
val allowedResources =
|
||||
request.resources.filter {
|
||||
it == PermissionRequest.RESOURCE_VIDEO_CAPTURE ||
|
||||
it == PermissionRequest.RESOURCE_AUDIO_CAPTURE
|
||||
}
|
||||
if (allowedResources.size != request.resources.size) {
|
||||
request.deny()
|
||||
return
|
||||
}
|
||||
val neededPermissions = mutableListOf<String>()
|
||||
if (request.resources.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
|
||||
if (allowedResources.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
|
||||
neededPermissions.add(Manifest.permission.CAMERA)
|
||||
}
|
||||
if (request.resources.contains(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) {
|
||||
if (allowedResources.contains(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) {
|
||||
neededPermissions.add(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
|
||||
@@ -173,7 +131,7 @@ class AndroidWebViewHost(
|
||||
return
|
||||
}
|
||||
|
||||
request.grant(request.resources)
|
||||
request.grant(allowedResources.toTypedArray())
|
||||
}
|
||||
|
||||
override fun onShowFileChooser(
|
||||
@@ -200,14 +158,9 @@ class AndroidWebViewHost(
|
||||
}
|
||||
}
|
||||
|
||||
addJavascriptInterface(BridgeJsInterface(), "SelfNativeAndroid")
|
||||
installBridge(webView = this)
|
||||
|
||||
if (isDebugMode) {
|
||||
loadUrl(buildDebugUrl(queryParams))
|
||||
} else {
|
||||
loadUrl(buildBundledUrl(queryParams))
|
||||
maybeLoadVerifiedRemoteContent(queryParams)
|
||||
}
|
||||
loadUrl(initialContentUrl(queryParams, isDebugMode, remoteWebAppBaseUrl))
|
||||
}
|
||||
return webView
|
||||
}
|
||||
@@ -226,123 +179,145 @@ class AndroidWebViewHost(
|
||||
webView.destroy()
|
||||
}
|
||||
|
||||
private fun buildBundledUrl(queryParams: String): String = buildEntryUrl(BUNDLED_ORIGIN, queryParams)
|
||||
|
||||
private fun buildDebugUrl(queryParams: String): String = buildEntryUrl(DEBUG_ORIGIN, queryParams)
|
||||
|
||||
private fun buildRemoteUrl(queryParams: String): String? = RemoteNavigationPolicy.buildRemoteEntryUrl(remoteWebAppBaseUrl, queryParams)
|
||||
|
||||
private fun buildEntryUrl(
|
||||
baseUrl: String,
|
||||
queryParams: String,
|
||||
): String {
|
||||
val separator = if (queryParams.isEmpty()) "" else "?$queryParams"
|
||||
return "$baseUrl/tunnel/tour/1$separator"
|
||||
}
|
||||
|
||||
private fun maybeLoadVerifiedRemoteContent(queryParams: String) {
|
||||
val remoteUrl = buildRemoteUrl(queryParams) ?: return
|
||||
val expectedSha256 = remoteWebAppIntegritySha256?.takeIf { it.isNotBlank() } ?: return
|
||||
|
||||
Thread {
|
||||
val verifiedHtml = fetchAndVerifyRemoteEntry(remoteUrl, expectedSha256)
|
||||
if (verifiedHtml == null || !::webView.isInitialized || isDestroyed) {
|
||||
return@Thread
|
||||
}
|
||||
|
||||
webView.post {
|
||||
if (::webView.isInitialized && !isDestroyed) {
|
||||
webView.loadDataWithBaseURL(
|
||||
remoteUrl,
|
||||
verifiedHtml,
|
||||
"text/html",
|
||||
"UTF-8",
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun fetchAndVerifyRemoteEntry(
|
||||
remoteUrl: String,
|
||||
expectedSha256: String,
|
||||
): String? =
|
||||
try {
|
||||
val connection = URL(remoteUrl).openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.instanceFollowRedirects = false
|
||||
connection.connectTimeout = 5_000
|
||||
connection.readTimeout = 5_000
|
||||
connection.connect()
|
||||
|
||||
if (connection.responseCode !in 200..299) {
|
||||
Log.w("WebViewHost", "Remote web app integrity check failed with HTTP ${connection.responseCode}")
|
||||
null
|
||||
} else if (!RemoteContentIntegrity.isAcceptableContentType(connection.contentType)) {
|
||||
Log.w("WebViewHost", "Remote web app integrity check failed due to unexpected content type ${connection.contentType}")
|
||||
null
|
||||
} else {
|
||||
val body =
|
||||
connection.inputStream.use { stream ->
|
||||
val buffer = java.io.ByteArrayOutputStream()
|
||||
val chunk = ByteArray(8192)
|
||||
var totalRead = 0
|
||||
var bytesRead: Int
|
||||
while (stream.read(chunk).also { bytesRead = it } != -1) {
|
||||
totalRead += bytesRead
|
||||
if (totalRead > MAX_REMOTE_ENTRY_BYTES) {
|
||||
throw IllegalStateException("Remote entry response exceeded ${MAX_REMOTE_ENTRY_BYTES} bytes")
|
||||
}
|
||||
buffer.write(chunk, 0, bytesRead)
|
||||
}
|
||||
buffer.toByteArray()
|
||||
}
|
||||
if (sha256Hex(body) == normalizeSha256(expectedSha256)) {
|
||||
String(body, Charsets.UTF_8)
|
||||
} else {
|
||||
Log.w("WebViewHost", "Remote web app integrity check failed: hash mismatch")
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
Log.w("WebViewHost", "Remote web app integrity check failed", error)
|
||||
null
|
||||
private fun installBridge(webView: WebView) {
|
||||
check(WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
|
||||
"WEB_MESSAGE_LISTENER not supported — native bridge unavailable on this device"
|
||||
}
|
||||
|
||||
private fun isAllowedRemoteOrigin(url: String): Boolean = RemoteNavigationPolicy.isAllowedRemoteOrigin(url, remoteWebAppBaseUrl)
|
||||
WebViewCompat.addWebMessageListener(
|
||||
webView,
|
||||
"SelfNativeAndroid",
|
||||
buildAllowedOriginRules(isDebugMode, remoteWebAppBaseUrl),
|
||||
) { _, message: WebMessageCompat, sourceOrigin, isMainFrame, _ ->
|
||||
if (!isMainFrame) {
|
||||
return@addWebMessageListener
|
||||
}
|
||||
|
||||
private fun resolvePort(uri: Uri): Int = RemoteNavigationPolicy.resolvePort(URI(uri.toString()))
|
||||
|
||||
private fun sha256Hex(bytes: ByteArray): String = RemoteContentIntegrity.sha256Hex(bytes)
|
||||
|
||||
private fun normalizeSha256(value: String): String = RemoteContentIntegrity.normalizeSha256(value)
|
||||
|
||||
inner class BridgeJsInterface {
|
||||
@JavascriptInterface
|
||||
fun postMessage(json: String) {
|
||||
router.onMessageReceived(json)
|
||||
val rawJson = message.data ?: return@addWebMessageListener
|
||||
router.onMessageReceived(
|
||||
rawJson = rawJson,
|
||||
isTrustedSource =
|
||||
isTrustedBridgeOrigin(
|
||||
sourceOrigin.toString(),
|
||||
isDebugMode,
|
||||
remoteWebAppBaseUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBundledOrigin(uri: Uri): Boolean = isMatchingOrigin(uri, "https", BUNDLED_HOST, 443)
|
||||
|
||||
private fun isDebugOrigin(uri: Uri): Boolean = isMatchingOrigin(uri, "http", "127.0.0.1", 5173)
|
||||
|
||||
private fun isMatchingOrigin(
|
||||
uri: Uri,
|
||||
scheme: String,
|
||||
host: String,
|
||||
port: Int,
|
||||
): Boolean = uri.scheme == scheme && uri.host == host && resolvePort(uri) == port
|
||||
|
||||
companion object {
|
||||
private const val BUNDLED_HOST = "appassets.androidplatform.net"
|
||||
private const val BUNDLED_ORIGIN = "https://$BUNDLED_HOST"
|
||||
private const val DEBUG_ORIGIN = "http://127.0.0.1:5173"
|
||||
|
||||
const val FILE_CHOOSER_REQUEST_CODE = 1001
|
||||
const val CAMERA_PERMISSION_REQUEST_CODE = 1002
|
||||
private const val MAX_REMOTE_ENTRY_BYTES = 5 * 1024 * 1024
|
||||
private const val DEFAULT_REMOTE_BASE_URL = "https://self-app-alpha.vercel.app"
|
||||
private const val BUNDLED_TOUR_PATH = "/tunnel/tour/1"
|
||||
private const val DEBUG_HOST = "127.0.0.1"
|
||||
private const val DEBUG_PORT = 5173
|
||||
private const val DIDIT_HOST = "verify.didit.me"
|
||||
|
||||
internal fun initialContentUrl(
|
||||
queryParams: String,
|
||||
isDebugMode: Boolean,
|
||||
remoteWebAppBaseUrl: String = DEFAULT_REMOTE_BASE_URL,
|
||||
): String =
|
||||
if (isDebugMode) {
|
||||
buildString {
|
||||
append("http://")
|
||||
.append(DEBUG_HOST)
|
||||
.append(":")
|
||||
.append(DEBUG_PORT)
|
||||
.append(BUNDLED_TOUR_PATH)
|
||||
if (queryParams.isNotEmpty()) {
|
||||
append("?").append(queryParams)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
require(remoteWebAppBaseUrl.startsWith("https://")) {
|
||||
"remoteWebAppBaseUrl must use HTTPS in release builds"
|
||||
}
|
||||
buildString {
|
||||
append(remoteWebAppBaseUrl.trimEnd('/')).append(BUNDLED_TOUR_PATH)
|
||||
if (queryParams.isNotEmpty()) {
|
||||
append("?").append(queryParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun isAllowedNavigationUrl(
|
||||
rawUrl: String?,
|
||||
isDebugMode: Boolean,
|
||||
remoteWebAppBaseUrl: String? = null,
|
||||
): Boolean =
|
||||
isRemoteOrigin(rawUrl, remoteWebAppBaseUrl) ||
|
||||
isDiditUrl(rawUrl) ||
|
||||
(isDebugMode && isDebugLocalUrl(rawUrl))
|
||||
|
||||
internal fun isTrustedPermissionOrigin(
|
||||
rawUrl: String?,
|
||||
isDebugMode: Boolean,
|
||||
remoteWebAppBaseUrl: String? = null,
|
||||
): Boolean =
|
||||
isRemoteOrigin(rawUrl, remoteWebAppBaseUrl) ||
|
||||
isDiditUrl(rawUrl) ||
|
||||
(isDebugMode && isDebugLocalUrl(rawUrl))
|
||||
|
||||
internal fun isTrustedBridgeOrigin(
|
||||
rawUrl: String?,
|
||||
isDebugMode: Boolean,
|
||||
remoteWebAppBaseUrl: String? = null,
|
||||
): Boolean =
|
||||
isRemoteOrigin(rawUrl, remoteWebAppBaseUrl) ||
|
||||
(isDebugMode && isDebugLocalUrl(rawUrl))
|
||||
|
||||
private fun isDiditUrl(rawUrl: String?): Boolean {
|
||||
val port = uriPort(rawUrl)
|
||||
return uriScheme(rawUrl) == "https" &&
|
||||
uriHost(rawUrl) == DIDIT_HOST &&
|
||||
(port == null || port == 443)
|
||||
}
|
||||
|
||||
private fun isDebugLocalUrl(rawUrl: String?): Boolean =
|
||||
uriScheme(rawUrl) == "http" && uriHost(rawUrl) == DEBUG_HOST && uriPort(rawUrl) == DEBUG_PORT
|
||||
|
||||
private fun buildAllowedOriginRules(
|
||||
isDebugMode: Boolean,
|
||||
remoteWebAppBaseUrl: String? = null,
|
||||
): Set<String> =
|
||||
buildSet {
|
||||
remoteWebAppBaseUrl
|
||||
?.let(::buildOriginRule)
|
||||
?.let(::add)
|
||||
if (isDebugMode) {
|
||||
add("http://$DEBUG_HOST:$DEBUG_PORT")
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildOriginRule(rawUrl: String): String? {
|
||||
val uri = parseUri(rawUrl) ?: return null
|
||||
val scheme = uri.scheme ?: return null
|
||||
if (scheme != "https") return null
|
||||
val host = uri.host ?: return null
|
||||
val port = uri.port.takeIf { it != -1 }
|
||||
|
||||
return buildString {
|
||||
append(scheme).append("://").append(host)
|
||||
if (port != null) {
|
||||
append(":").append(port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRemoteOrigin(
|
||||
rawUrl: String?,
|
||||
remoteWebAppBaseUrl: String?,
|
||||
): Boolean = rawUrl?.let { RemoteNavigationPolicy.isAllowedRemoteOrigin(it, remoteWebAppBaseUrl) } ?: false
|
||||
|
||||
private fun uriScheme(rawUrl: String?): String? = parseUri(rawUrl)?.scheme
|
||||
|
||||
private fun uriHost(rawUrl: String?): String? = parseUri(rawUrl)?.host ?: parseUri(rawUrl)?.authority
|
||||
|
||||
private fun uriPort(rawUrl: String?): Int? = parseUri(rawUrl)?.port?.takeIf { it != -1 }
|
||||
|
||||
private fun parseUri(rawUrl: String?): java.net.URI? = rawUrl?.let { raw -> runCatching { java.net.URI(raw) }.getOrNull() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package xyz.self.sdk.webview
|
||||
|
||||
import java.security.MessageDigest
|
||||
|
||||
// null contentType is allowed — some CDNs omit Content-Type; the SHA-256 hash is the primary integrity gate.
|
||||
internal object RemoteContentIntegrity {
|
||||
fun normalizeSha256(value: String): String = value.lowercase().removePrefix("sha256-").removePrefix("0x")
|
||||
|
||||
fun sha256Hex(bytes: ByteArray): String =
|
||||
MessageDigest.getInstance("SHA-256").digest(bytes).joinToString("") { byte ->
|
||||
"%02x".format(byte)
|
||||
}
|
||||
|
||||
fun isAcceptableContentType(rawContentType: String?): Boolean {
|
||||
val normalized = rawContentType?.substringBefore(";")?.trim()?.lowercase()
|
||||
return normalized == null || normalized == "text/html"
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,13 @@ package xyz.self.sdk.webview
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebChromeClient
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import xyz.self.sdk.api.SelfSdk
|
||||
import xyz.self.sdk.bridge.MessageRouter
|
||||
import xyz.self.sdk.handlers.CryptoHandler
|
||||
@@ -16,9 +21,11 @@ import xyz.self.sdk.handlers.SecureStorageHandler
|
||||
class SelfVerificationActivity : AppCompatActivity() {
|
||||
private lateinit var webViewHost: AndroidWebViewHost
|
||||
private lateinit var router: MessageRouter
|
||||
private var container: FrameLayout? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
val isDebugMode = intent.getBooleanExtra(EXTRA_DEBUG_MODE, false)
|
||||
val environment = intent.getStringExtra(EXTRA_ENVIRONMENT) ?: "prod"
|
||||
@@ -36,8 +43,7 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
val chainID = if (intent.hasExtra(EXTRA_CHAIN_ID)) intent.getIntExtra(EXTRA_CHAIN_ID, 0) else null
|
||||
val userDefinedData = intent.getStringExtra(EXTRA_USER_DEFINED_DATA)
|
||||
val selfDefinedData = intent.getStringExtra(EXTRA_SELF_DEFINED_DATA)
|
||||
val remoteWebAppBaseUrl = intent.getStringExtra(EXTRA_REMOTE_WEB_APP_BASE_URL)
|
||||
val remoteWebAppIntegritySha256 = intent.getStringExtra(EXTRA_REMOTE_WEB_APP_INTEGRITY_SHA256)
|
||||
val remoteWebAppBaseUrl = intent.getStringExtra(EXTRA_REMOTE_WEB_APP_BASE_URL) ?: "https://self-app-alpha.vercel.app"
|
||||
|
||||
router =
|
||||
MessageRouter(
|
||||
@@ -68,7 +74,6 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
router = router,
|
||||
isDebugMode = isDebugMode,
|
||||
remoteWebAppBaseUrl = remoteWebAppBaseUrl,
|
||||
remoteWebAppIntegritySha256 = remoteWebAppIntegritySha256,
|
||||
)
|
||||
|
||||
val queryParams =
|
||||
@@ -95,7 +100,32 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
val webView = webViewHost.createWebView(queryParams)
|
||||
setContentView(webView)
|
||||
val wrapper =
|
||||
FrameLayout(this).apply {
|
||||
addView(
|
||||
webView,
|
||||
ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
),
|
||||
)
|
||||
}
|
||||
container = wrapper
|
||||
setContentView(wrapper)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(wrapper) { view, insets ->
|
||||
val systemInsets =
|
||||
insets.getInsets(
|
||||
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(),
|
||||
)
|
||||
view.setPadding(
|
||||
systemInsets.left,
|
||||
systemInsets.top,
|
||||
systemInsets.right,
|
||||
systemInsets.bottom,
|
||||
)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
@@ -137,6 +167,7 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
container?.let { ViewCompat.setOnApplyWindowInsetsListener(it, null) }
|
||||
if (::webViewHost.isInitialized) {
|
||||
webViewHost.destroy()
|
||||
}
|
||||
@@ -161,7 +192,6 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
const val EXTRA_USER_DEFINED_DATA = "xyz.self.sdk.USER_DEFINED_DATA"
|
||||
const val EXTRA_SELF_DEFINED_DATA = "xyz.self.sdk.SELF_DEFINED_DATA"
|
||||
const val EXTRA_REMOTE_WEB_APP_BASE_URL = "xyz.self.sdk.REMOTE_WEB_APP_BASE_URL"
|
||||
const val EXTRA_REMOTE_WEB_APP_INTEGRITY_SHA256 = "xyz.self.sdk.REMOTE_WEB_APP_INTEGRITY_SHA256"
|
||||
const val EXTRA_RESULT_DATA = "xyz.self.sdk.RESULT_DATA"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class MessageRouterTest {
|
||||
val sent = mutableListOf<String>()
|
||||
val router = MessageRouter(sendToWebView = { sent.add(it) }, scope = this)
|
||||
|
||||
router.onMessageReceived(makeRequest(version = 999))
|
||||
router.onMessageReceived(makeRequest(version = 999), isTrustedSource = true)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(1, sent.size)
|
||||
@@ -82,7 +82,7 @@ class MessageRouterTest {
|
||||
val sent = mutableListOf<String>()
|
||||
val router = MessageRouter(sendToWebView = { sent.add(it) }, scope = this)
|
||||
|
||||
router.onMessageReceived(makeRequest(domain = BridgeDomain.NFC))
|
||||
router.onMessageReceived(makeRequest(domain = BridgeDomain.NFC), isTrustedSource = true)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(1, sent.size)
|
||||
@@ -98,7 +98,7 @@ class MessageRouterTest {
|
||||
val router = MessageRouter(sendToWebView = { sent.add(it) }, scope = this)
|
||||
router.register(StubHandler(BridgeDomain.SECURE_STORAGE, result = JsonPrimitive("ok")))
|
||||
|
||||
router.onMessageReceived(makeRequest())
|
||||
router.onMessageReceived(makeRequest(), isTrustedSource = true)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(1, sent.size)
|
||||
@@ -119,7 +119,7 @@ class MessageRouterTest {
|
||||
),
|
||||
)
|
||||
|
||||
router.onMessageReceived(makeRequest())
|
||||
router.onMessageReceived(makeRequest(), isTrustedSource = true)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(1, sent.size)
|
||||
@@ -141,7 +141,7 @@ class MessageRouterTest {
|
||||
),
|
||||
)
|
||||
|
||||
router.onMessageReceived(makeRequest())
|
||||
router.onMessageReceived(makeRequest(), isTrustedSource = true)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(1, sent.size)
|
||||
@@ -157,7 +157,21 @@ class MessageRouterTest {
|
||||
val sent = mutableListOf<String>()
|
||||
val router = MessageRouter(sendToWebView = { sent.add(it) }, scope = this)
|
||||
|
||||
router.onMessageReceived("{not valid json")
|
||||
router.onMessageReceived("{not valid json", isTrustedSource = true)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertTrue(sent.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `messages from untrusted origins are dropped before dispatch`() =
|
||||
runTest {
|
||||
val sent = mutableListOf<String>()
|
||||
val handler = StubHandler(BridgeDomain.SECURE_STORAGE, result = JsonPrimitive("ok"))
|
||||
val router = MessageRouter(sendToWebView = { sent.add(it) }, scope = this)
|
||||
router.register(handler)
|
||||
|
||||
router.onMessageReceived(makeRequest(), isTrustedSource = false)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertTrue(sent.isEmpty())
|
||||
@@ -171,7 +185,7 @@ class MessageRouterTest {
|
||||
router.register(StubHandler(BridgeDomain.SECURE_STORAGE, result = JsonPrimitive("first")))
|
||||
router.register(StubHandler(BridgeDomain.SECURE_STORAGE, result = JsonPrimitive("second")))
|
||||
|
||||
router.onMessageReceived(makeRequest())
|
||||
router.onMessageReceived(makeRequest(), isTrustedSource = true)
|
||||
advanceUntilIdle()
|
||||
|
||||
val resp = parseResponse(sent[0])
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package xyz.self.sdk.webview
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class AndroidWebViewHostSecurityTest {
|
||||
@Test
|
||||
fun `release builds launch remote content`() {
|
||||
assertEquals(
|
||||
"https://self-app-alpha.vercel.app/tunnel/tour/1",
|
||||
AndroidWebViewHost.initialContentUrl(queryParams = "", isDebugMode = false),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `debug builds launch localhost`() {
|
||||
assertTrue(
|
||||
AndroidWebViewHost
|
||||
.initialContentUrl(queryParams = "", isDebugMode = true)
|
||||
.startsWith("http://127.0.0.1:5173"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `navigation allows remote didit and debug origins`() {
|
||||
val remoteBase = "https://self-app-alpha.vercel.app"
|
||||
assertTrue(
|
||||
AndroidWebViewHost.isAllowedNavigationUrl(
|
||||
"https://self-app-alpha.vercel.app/tunnel/tour/1",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteBase,
|
||||
),
|
||||
)
|
||||
assertTrue(
|
||||
AndroidWebViewHost.isAllowedNavigationUrl(
|
||||
"https://verify.didit.me/session/123",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteBase,
|
||||
),
|
||||
)
|
||||
assertFalse(
|
||||
AndroidWebViewHost.isAllowedNavigationUrl(
|
||||
"https://evil.example.com/tunnel/tour/1",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteBase,
|
||||
),
|
||||
)
|
||||
assertTrue(
|
||||
AndroidWebViewHost.isAllowedNavigationUrl(
|
||||
"http://127.0.0.1:5173/tunnel/tour/1",
|
||||
isDebugMode = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `release build rejects HTTP base URL`() {
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
AndroidWebViewHost.initialContentUrl(
|
||||
queryParams = "",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = "http://self-app-alpha.vercel.app",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `didit on non-443 port is rejected`() {
|
||||
val remoteBase = "https://self-app-alpha.vercel.app"
|
||||
assertFalse(
|
||||
AndroidWebViewHost.isAllowedNavigationUrl(
|
||||
"https://verify.didit.me:8443/session/123",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteBase,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bridge trust accepts remote rejects didit and arbitrary origins`() {
|
||||
val remoteBase = "https://self-app-alpha.vercel.app"
|
||||
assertTrue(
|
||||
AndroidWebViewHost.isTrustedBridgeOrigin(
|
||||
"https://self-app-alpha.vercel.app/tunnel/tour/1",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteBase,
|
||||
),
|
||||
)
|
||||
assertFalse(
|
||||
AndroidWebViewHost.isTrustedBridgeOrigin(
|
||||
"https://verify.didit.me/session/123",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteBase,
|
||||
),
|
||||
)
|
||||
assertFalse(
|
||||
AndroidWebViewHost.isTrustedBridgeOrigin(
|
||||
"https://evil.example.com/tunnel/tour/1",
|
||||
isDebugMode = false,
|
||||
remoteWebAppBaseUrl = remoteBase,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package xyz.self.sdk.webview
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class RemoteContentIntegrityTest {
|
||||
// -- normalizeSha256 --
|
||||
|
||||
@Test
|
||||
fun `normalizeSha256 strips sha256- prefix`() {
|
||||
assertEquals(
|
||||
"abcdef1234567890",
|
||||
RemoteContentIntegrity.normalizeSha256("sha256-abcdef1234567890"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `normalizeSha256 strips 0x prefix`() {
|
||||
assertEquals(
|
||||
"abcdef1234567890",
|
||||
RemoteContentIntegrity.normalizeSha256("0xabcdef1234567890"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `normalizeSha256 strips sha256- then 0x prefix`() {
|
||||
assertEquals(
|
||||
"abcdef",
|
||||
RemoteContentIntegrity.normalizeSha256("sha256-0xabcdef"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `normalizeSha256 lowercases input`() {
|
||||
assertEquals(
|
||||
"abcdef",
|
||||
RemoteContentIntegrity.normalizeSha256("ABCDEF"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `normalizeSha256 returns raw hex unchanged`() {
|
||||
assertEquals(
|
||||
"abcdef1234567890",
|
||||
RemoteContentIntegrity.normalizeSha256("abcdef1234567890"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `normalizeSha256 does not strip interior sha256-`() {
|
||||
assertEquals(
|
||||
"absha256-cd",
|
||||
RemoteContentIntegrity.normalizeSha256("absha256-cd"),
|
||||
)
|
||||
}
|
||||
|
||||
// -- sha256Hex --
|
||||
|
||||
@Test
|
||||
fun `sha256Hex produces correct hash for known input`() {
|
||||
// SHA-256 of empty byte array
|
||||
assertEquals(
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
RemoteContentIntegrity.sha256Hex(byteArrayOf()),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sha256Hex produces correct hash for hello`() {
|
||||
assertEquals(
|
||||
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
|
||||
RemoteContentIntegrity.sha256Hex("hello".toByteArray(Charsets.UTF_8)),
|
||||
)
|
||||
}
|
||||
|
||||
// -- isAcceptableContentType --
|
||||
|
||||
@Test
|
||||
fun `accepts null content type`() {
|
||||
assertTrue(RemoteContentIntegrity.isAcceptableContentType(null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejects empty content type`() {
|
||||
assertFalse(RemoteContentIntegrity.isAcceptableContentType(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `accepts text html`() {
|
||||
assertTrue(RemoteContentIntegrity.isAcceptableContentType("text/html"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `accepts text html with charset`() {
|
||||
assertTrue(RemoteContentIntegrity.isAcceptableContentType("text/html; charset=utf-8"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `accepts Text HTML case insensitive`() {
|
||||
assertTrue(RemoteContentIntegrity.isAcceptableContentType("Text/HTML"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejects application javascript`() {
|
||||
assertFalse(RemoteContentIntegrity.isAcceptableContentType("application/javascript"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejects application json`() {
|
||||
assertFalse(RemoteContentIntegrity.isAcceptableContentType("application/json"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejects text plain`() {
|
||||
assertFalse(RemoteContentIntegrity.isAcceptableContentType("text/plain"))
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,8 @@ let package = Package(
|
||||
.target(
|
||||
name: "SelfNativeShell",
|
||||
path: ".",
|
||||
sources: ["Sources/SelfNativeShell"],
|
||||
resources: [
|
||||
.copy("Resources/self-sdk-web")
|
||||
]
|
||||
exclude: ["Resources"],
|
||||
sources: ["Sources/SelfNativeShell"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "SelfNativeShellTests",
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Self</title>
|
||||
<script type="module" crossorigin src="./assets/index-YX6AnLbA.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-LqDWjDzu.css">
|
||||
<script type="module" crossorigin src="/assets/index-YX6AnLbA.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-LqDWjDzu.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -65,8 +65,7 @@ final class SelfSdkViewController: UIViewController {
|
||||
let host = SelfWebViewHost(
|
||||
router: router,
|
||||
isDebugMode: config.isDebugMode,
|
||||
remoteWebAppBaseURL: config.remoteWebAppBaseURL,
|
||||
remoteWebAppIntegritySha256: config.remoteWebAppIntegritySha256
|
||||
remoteWebAppBaseURL: config.remoteWebAppBaseURL
|
||||
)
|
||||
self.webViewHost = host
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ public struct SelfSdkConfig {
|
||||
public let userDefinedData: String?
|
||||
public let selfDefinedData: String?
|
||||
public let remoteWebAppBaseURL: URL?
|
||||
public let remoteWebAppIntegritySha256: String?
|
||||
public let secureStorageProvider: SecureStorageProvider
|
||||
|
||||
public init(
|
||||
@@ -41,7 +40,6 @@ public struct SelfSdkConfig {
|
||||
userDefinedData: String? = nil,
|
||||
selfDefinedData: String? = nil,
|
||||
remoteWebAppBaseURL: URL? = nil,
|
||||
remoteWebAppIntegritySha256: String? = nil,
|
||||
secureStorageProvider: SecureStorageProvider
|
||||
) {
|
||||
self.verificationId = verificationId
|
||||
@@ -61,7 +59,6 @@ public struct SelfSdkConfig {
|
||||
self.userDefinedData = userDefinedData
|
||||
self.selfDefinedData = selfDefinedData
|
||||
self.remoteWebAppBaseURL = remoteWebAppBaseURL
|
||||
self.remoteWebAppIntegritySha256 = remoteWebAppIntegritySha256
|
||||
self.secureStorageProvider = secureStorageProvider
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@ final class MessageRouter {
|
||||
handlers[handler.domain] = handler
|
||||
}
|
||||
|
||||
func onMessageReceived(rawJson: String) {
|
||||
func onMessageReceived(rawJson: String, isTrustedSource: Bool) {
|
||||
guard isTrustedSource else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = rawJson.data(using: .utf8) else {
|
||||
return
|
||||
}
|
||||
@@ -47,7 +51,10 @@ final class MessageRouter {
|
||||
}
|
||||
|
||||
let params = request.params?.mapValues { $0.value }
|
||||
dispatchRequest(request, handler: handler, params: params)
|
||||
}
|
||||
|
||||
private func dispatchRequest(_ request: BridgeRequest, handler: BridgeHandler, params: [String: Any]?) {
|
||||
Task {
|
||||
do {
|
||||
let result = try await handler.handle(method: request.method, params: params)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
import Foundation
|
||||
|
||||
enum BundledAssetPathResolver {
|
||||
static func resolveFileURL(for requestURL: URL, rootURL: URL) -> URL? {
|
||||
let rawPath = requestURL.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let normalizedPath = rawPath.removingPercentEncoding ?? rawPath
|
||||
let relativePath = normalizedPath.isEmpty || !normalizedPath.contains(".") ? "index.html" : normalizedPath
|
||||
let fileURL = rootURL.appendingPathComponent(relativePath, isDirectory: false).standardized
|
||||
let standardizedRootURL = rootURL.standardizedFileURL
|
||||
let rootPath = standardizedRootURL.path.hasSuffix("/")
|
||||
? standardizedRootURL.path
|
||||
: standardizedRootURL.path + "/"
|
||||
guard fileURL.path.hasPrefix(rootPath) || fileURL.path == standardizedRootURL.path else {
|
||||
return nil
|
||||
}
|
||||
return fileURL
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
import Foundation
|
||||
|
||||
// nil mimeType is allowed — some CDNs omit Content-Type; the SHA-256 hash is the primary integrity gate.
|
||||
enum RemoteContentIntegrity {
|
||||
static func normalizeSha256(_ value: String) -> String {
|
||||
var normalized = value.lowercased()
|
||||
if normalized.hasPrefix("sha256-") {
|
||||
normalized.removeFirst("sha256-".count)
|
||||
}
|
||||
if normalized.hasPrefix("0x") {
|
||||
normalized.removeFirst(2)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
static func isAcceptableMimeType(_ mimeType: String?) -> Bool {
|
||||
mimeType == nil || mimeType == "text/html"
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@
|
||||
import Foundation
|
||||
|
||||
enum RemoteNavigationPolicy {
|
||||
private static let bundledScheme = SelfWebViewHost.bundledScheme
|
||||
private static let bundledHost = SelfWebViewHost.bundledHost
|
||||
private static let allowedSubframeHosts: Set<String> = ["verify.didit.me"]
|
||||
|
||||
static func makeEntryURL(baseURL: URL?, queryParams: String) -> URL? {
|
||||
@@ -25,11 +23,9 @@ enum RemoteNavigationPolicy {
|
||||
isDebugMode: Bool
|
||||
) -> Bool {
|
||||
if isDebugMode {
|
||||
return url.absoluteString.hasPrefix("http://localhost:5173")
|
||||
}
|
||||
|
||||
if url.scheme == bundledScheme, url.host == bundledHost {
|
||||
return true
|
||||
return url.scheme == "http" &&
|
||||
url.host == "localhost" &&
|
||||
resolvedPort(for: url) == 5173
|
||||
}
|
||||
|
||||
guard let remoteWebAppBaseURL,
|
||||
@@ -54,7 +50,8 @@ enum RemoteNavigationPolicy {
|
||||
guard url.scheme == "https", let host = url.host else {
|
||||
return false
|
||||
}
|
||||
return allowedSubframeHosts.contains(host)
|
||||
let port = resolvedPort(for: url)
|
||||
return allowedSubframeHosts.contains(host) && port == 443
|
||||
}
|
||||
|
||||
static func resolvedPort(for url: URL) -> Int {
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
final class SelfWebViewHost: NSObject {
|
||||
static let bundledScheme = "self-sdk"
|
||||
static let bundledHost = "app"
|
||||
fileprivate static let bundledRootFolder = "self-sdk-web"
|
||||
private static let defaultRemoteBaseURL = URL(string: "https://self-app-alpha.vercel.app")!
|
||||
|
||||
private var webView: WKWebView?
|
||||
private let router: MessageRouter
|
||||
private let isDebugMode: Bool
|
||||
private let remoteWebAppBaseURL: URL?
|
||||
private let remoteWebAppIntegritySha256: String?
|
||||
private let remoteWebAppBaseURL: URL
|
||||
|
||||
init(
|
||||
router: MessageRouter,
|
||||
isDebugMode: Bool = false,
|
||||
remoteWebAppBaseURL: URL? = nil,
|
||||
remoteWebAppIntegritySha256: String? = nil
|
||||
remoteWebAppBaseURL: URL? = nil
|
||||
) {
|
||||
self.router = router
|
||||
self.isDebugMode = isDebugMode
|
||||
self.remoteWebAppBaseURL = remoteWebAppBaseURL
|
||||
self.remoteWebAppIntegritySha256 = remoteWebAppIntegritySha256
|
||||
self.remoteWebAppBaseURL = remoteWebAppBaseURL ?? Self.defaultRemoteBaseURL
|
||||
super.init()
|
||||
}
|
||||
|
||||
@@ -37,7 +31,6 @@ final class SelfWebViewHost: NSObject {
|
||||
config.preferences.javaScriptCanOpenWindowsAutomatically = false
|
||||
config.allowsInlineMediaPlayback = true
|
||||
config.mediaTypesRequiringUserActionForPlayback = []
|
||||
config.setURLSchemeHandler(SelfBundledAssetSchemeHandler(), forURLScheme: SelfWebViewHost.bundledScheme)
|
||||
|
||||
let webView = WKWebView(frame: .zero, configuration: config)
|
||||
webView.scrollView.bounces = false
|
||||
@@ -49,14 +42,12 @@ final class SelfWebViewHost: NSObject {
|
||||
webView.isInspectable = isDebugMode
|
||||
}
|
||||
|
||||
webView.navigationDelegate = self
|
||||
self.webView = webView
|
||||
return webView
|
||||
}
|
||||
|
||||
func loadContent(queryParams: String) {
|
||||
guard let webView = webView else { return }
|
||||
|
||||
if isDebugMode {
|
||||
let debugBase = URL(string: "http://localhost:5173")
|
||||
if let url = RemoteNavigationPolicy.makeEntryURL(baseURL: debugBase, queryParams: queryParams) {
|
||||
@@ -64,12 +55,10 @@ final class SelfWebViewHost: NSObject {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let bundledURL = makeBundledEntryURL(queryParams: queryParams) {
|
||||
webView.load(URLRequest(url: bundledURL))
|
||||
guard remoteWebAppBaseURL.scheme == "https" else { return }
|
||||
if let url = RemoteNavigationPolicy.makeEntryURL(baseURL: remoteWebAppBaseURL, queryParams: queryParams) {
|
||||
webView.load(URLRequest(url: url))
|
||||
}
|
||||
|
||||
loadVerifiedRemoteContent(queryParams: queryParams)
|
||||
}
|
||||
|
||||
func evaluateJs(_ js: String) {
|
||||
@@ -78,71 +67,6 @@ final class SelfWebViewHost: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func makeBundledEntryURL(queryParams: String) -> URL? {
|
||||
RemoteNavigationPolicy.makeEntryURL(
|
||||
baseURL: URL(string: "\(SelfWebViewHost.bundledScheme)://\(SelfWebViewHost.bundledHost)"),
|
||||
queryParams: queryParams
|
||||
)
|
||||
}
|
||||
|
||||
private func loadVerifiedRemoteContent(queryParams: String) {
|
||||
guard let baseURL = remoteWebAppBaseURL,
|
||||
baseURL.scheme == "https",
|
||||
baseURL.host != nil,
|
||||
let expectedSha256 = remoteWebAppIntegritySha256?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!expectedSha256.isEmpty,
|
||||
let remoteURL = RemoteNavigationPolicy.makeEntryURL(baseURL: baseURL, queryParams: queryParams) else {
|
||||
return
|
||||
}
|
||||
|
||||
Task.detached { [weak self] in
|
||||
guard let self else { return }
|
||||
guard let verifiedHTML = await self.fetchAndVerifyRemoteEntry(
|
||||
url: remoteURL, expectedSha256: expectedSha256
|
||||
) else {
|
||||
return
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
self.webView?.loadHTMLString(verifiedHTML, baseURL: remoteURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static let maxRemoteEntryBytes = 5 * 1024 * 1024
|
||||
|
||||
private func fetchAndVerifyRemoteEntry(url: URL, expectedSha256: String) async -> String? {
|
||||
do {
|
||||
let configuration = URLSessionConfiguration.ephemeral
|
||||
configuration.timeoutIntervalForRequest = 5
|
||||
configuration.timeoutIntervalForResource = 5
|
||||
let session = URLSession(configuration: configuration)
|
||||
let (data, response) = try await session.data(from: url)
|
||||
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
|
||||
return nil
|
||||
}
|
||||
guard RemoteContentIntegrity.isAcceptableMimeType(response.mimeType) else {
|
||||
return nil
|
||||
}
|
||||
guard data.count <= SelfWebViewHost.maxRemoteEntryBytes else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let digest = SHA256.hash(data: data)
|
||||
let actualHash = digest.map { String(format: "%02x", $0) }.joined()
|
||||
guard actualHash == normalizeSha256(expectedSha256) else {
|
||||
return nil
|
||||
}
|
||||
return String(data: data, encoding: .utf8)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func normalizeSha256(_ value: String) -> String {
|
||||
RemoteContentIntegrity.normalizeSha256(value)
|
||||
}
|
||||
|
||||
private func isAllowedNavigation(url: URL) -> Bool {
|
||||
RemoteNavigationPolicy.isAllowedMainFrameNavigation(
|
||||
url: url,
|
||||
@@ -190,84 +114,63 @@ extension SelfWebViewHost: WKScriptMessageHandler {
|
||||
didReceive message: WKScriptMessage
|
||||
) {
|
||||
guard message.name == "SelfNativeIOS",
|
||||
message.frameInfo.isMainFrame,
|
||||
isTrustedBridgeFrameInfo(message.frameInfo.securityOrigin),
|
||||
let body = message.body as? String else {
|
||||
return
|
||||
}
|
||||
router.onMessageReceived(rawJson: body)
|
||||
router.onMessageReceived(rawJson: body, isTrustedSource: true)
|
||||
}
|
||||
}
|
||||
|
||||
private final class SelfBundledAssetSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
|
||||
guard let requestURL = urlSchemeTask.request.url,
|
||||
let rootURL = Bundle.module.resourceURL?.appendingPathComponent(
|
||||
SelfWebViewHost.bundledRootFolder,
|
||||
isDirectory: true
|
||||
),
|
||||
let fileURL = resolveFileURL(for: requestURL, rootURL: rootURL) else {
|
||||
urlSchemeTask.didFailWithError(NSError(domain: NSURLErrorDomain, code: NSURLErrorFileDoesNotExist))
|
||||
return
|
||||
private extension SelfWebViewHost {
|
||||
func isTrustedBridgeOrigin(_ url: URL?) -> Bool {
|
||||
guard let url else { return false }
|
||||
if isDebugMode {
|
||||
return url.scheme == "http" &&
|
||||
url.host == "localhost" &&
|
||||
resolvedPort(for: url) == 5173
|
||||
}
|
||||
return url.scheme == remoteWebAppBaseURL.scheme &&
|
||||
url.host == remoteWebAppBaseURL.host &&
|
||||
resolvedPort(for: url) == resolvedPort(for: remoteWebAppBaseURL)
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
let response = URLResponse(
|
||||
url: requestURL,
|
||||
mimeType: mimeType(for: fileURL.pathExtension),
|
||||
expectedContentLength: data.count,
|
||||
textEncodingName: textEncodingName(for: fileURL.pathExtension)
|
||||
)
|
||||
urlSchemeTask.didReceive(response)
|
||||
urlSchemeTask.didReceive(data)
|
||||
urlSchemeTask.didFinish()
|
||||
} catch {
|
||||
urlSchemeTask.didFailWithError(error)
|
||||
extension SelfWebViewHost {
|
||||
func initialContentURL(queryParams: String) -> URL? {
|
||||
if isDebugMode {
|
||||
let debugBase = URL(string: "http://localhost:5173")
|
||||
return RemoteNavigationPolicy.makeEntryURL(baseURL: debugBase, queryParams: queryParams)
|
||||
}
|
||||
guard remoteWebAppBaseURL.scheme == "https" else { return nil }
|
||||
return RemoteNavigationPolicy.makeEntryURL(baseURL: remoteWebAppBaseURL, queryParams: queryParams)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {}
|
||||
|
||||
private func resolveFileURL(for requestURL: URL, rootURL: URL) -> URL? {
|
||||
BundledAssetPathResolver.resolveFileURL(for: requestURL, rootURL: rootURL)
|
||||
func isAllowedNavigationURL(_ url: URL?, host: String? = nil) -> Bool {
|
||||
guard let url else { return false }
|
||||
return isAllowedNavigation(url: url) || isAllowedSubframeNavigation(url: url)
|
||||
}
|
||||
|
||||
private func mimeType(for pathExtension: String) -> String {
|
||||
switch pathExtension.lowercased() {
|
||||
case "html":
|
||||
return "text/html"
|
||||
case "js":
|
||||
return "application/javascript"
|
||||
case "css":
|
||||
return "text/css"
|
||||
case "json":
|
||||
return "application/json"
|
||||
case "svg":
|
||||
return "image/svg+xml"
|
||||
case "png":
|
||||
return "image/png"
|
||||
case "jpg", "jpeg":
|
||||
return "image/jpeg"
|
||||
case "woff2":
|
||||
return "font/woff2"
|
||||
case "woff":
|
||||
return "font/woff"
|
||||
case "ttf":
|
||||
return "font/ttf"
|
||||
case "otf":
|
||||
return "font/otf"
|
||||
case "wav":
|
||||
return "audio/wav"
|
||||
default:
|
||||
return "application/octet-stream"
|
||||
func isTrustedBridgeURL(_ url: URL?) -> Bool {
|
||||
isTrustedBridgeOrigin(url)
|
||||
}
|
||||
|
||||
func isTrustedBridgeFrameInfo(_ origin: WKSecurityOrigin) -> Bool {
|
||||
if isDebugMode {
|
||||
return origin.protocol == "http" && origin.host == "localhost" && origin.port == 5173
|
||||
}
|
||||
return origin.protocol == remoteWebAppBaseURL.scheme &&
|
||||
origin.host == remoteWebAppBaseURL.host &&
|
||||
resolvedSecurityOriginPort(origin) == resolvedPort(for: remoteWebAppBaseURL)
|
||||
}
|
||||
|
||||
private func textEncodingName(for pathExtension: String) -> String? {
|
||||
switch pathExtension.lowercased() {
|
||||
case "html", "js", "css", "json", "svg":
|
||||
return "utf-8"
|
||||
default:
|
||||
return nil
|
||||
private func resolvedSecurityOriginPort(_ origin: WKSecurityOrigin) -> Int {
|
||||
if origin.port != 0 { return origin.port }
|
||||
switch origin.protocol {
|
||||
case "https": return 443
|
||||
case "http": return 80
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import SelfNativeShell
|
||||
|
||||
final class BundledAssetPathResolverTests: XCTestCase {
|
||||
func testResolvesIndexForDirectoryStyleRequest() {
|
||||
let rootURL = URL(fileURLWithPath: "/tmp/self-sdk-web", isDirectory: true)
|
||||
let requestURL = URL(string: "self-sdk://app/tunnel/tour/1")!
|
||||
|
||||
XCTAssertEqual(
|
||||
BundledAssetPathResolver.resolveFileURL(for: requestURL, rootURL: rootURL)?.path,
|
||||
rootURL.appendingPathComponent("index.html").path
|
||||
)
|
||||
}
|
||||
|
||||
func testResolvesStaticAssetInsideBundle() {
|
||||
let rootURL = URL(fileURLWithPath: "/tmp/self-sdk-web", isDirectory: true)
|
||||
let requestURL = URL(string: "self-sdk://app/assets/app.js")!
|
||||
|
||||
XCTAssertEqual(
|
||||
BundledAssetPathResolver.resolveFileURL(for: requestURL, rootURL: rootURL)?.path,
|
||||
rootURL.appendingPathComponent("assets/app.js").path
|
||||
)
|
||||
}
|
||||
|
||||
func testRejectsPathTraversal() {
|
||||
let rootURL = URL(fileURLWithPath: "/tmp/self-sdk-web", isDirectory: true)
|
||||
let requestURL = URL(string: "self-sdk://app/../../secret.txt")!
|
||||
|
||||
XCTAssertNil(BundledAssetPathResolver.resolveFileURL(for: requestURL, rootURL: rootURL))
|
||||
}
|
||||
|
||||
func testRejectsPercentEncodedTraversal() {
|
||||
let rootURL = URL(fileURLWithPath: "/tmp/self-sdk-web", isDirectory: true)
|
||||
let requestURL = URL(string: "self-sdk://app/%2E%2E/%2E%2E/secret.txt")!
|
||||
|
||||
XCTAssertNil(BundledAssetPathResolver.resolveFileURL(for: requestURL, rootURL: rootURL))
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,8 @@ final class MessageRouterEscapeTests: XCTestCase {
|
||||
router.onMessageReceived(
|
||||
rawJson: """
|
||||
{"type":"request","version":1,"id":"req-1","domain":"secureStorage","method":"get","timestamp":1000}
|
||||
"""
|
||||
""",
|
||||
isTrustedSource: true
|
||||
)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
@@ -38,7 +38,7 @@ final class MessageRouterTests: XCTestCase {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
router.onMessageReceived(rawJson: makeRequestJSON(version: 999))
|
||||
router.onMessageReceived(rawJson: makeRequestJSON(version: 999), isTrustedSource: true)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
@@ -57,7 +57,7 @@ final class MessageRouterTests: XCTestCase {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
router.onMessageReceived(rawJson: makeRequestJSON(domain: "secureStorage"))
|
||||
router.onMessageReceived(rawJson: makeRequestJSON(domain: "secureStorage"), isTrustedSource: true)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
@@ -77,7 +77,7 @@ final class MessageRouterTests: XCTestCase {
|
||||
}
|
||||
router.register(handler: StubHandler(domain: .secureStorage, result: ["value": "abc"]))
|
||||
|
||||
router.onMessageReceived(rawJson: makeRequestJSON())
|
||||
router.onMessageReceived(rawJson: makeRequestJSON(), isTrustedSource: true)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
@@ -99,7 +99,7 @@ final class MessageRouterTests: XCTestCase {
|
||||
error: BridgeHandlerError.missingParam("key")
|
||||
))
|
||||
|
||||
router.onMessageReceived(rawJson: makeRequestJSON())
|
||||
router.onMessageReceived(rawJson: makeRequestJSON(), isTrustedSource: true)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
@@ -120,7 +120,7 @@ final class MessageRouterTests: XCTestCase {
|
||||
error: NSError(domain: "test", code: 1, userInfo: [NSLocalizedDescriptionKey: "disk full"])
|
||||
))
|
||||
|
||||
router.onMessageReceived(rawJson: makeRequestJSON())
|
||||
router.onMessageReceived(rawJson: makeRequestJSON(), isTrustedSource: true)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
@@ -137,7 +137,7 @@ final class MessageRouterTests: XCTestCase {
|
||||
sentCount += 1
|
||||
}
|
||||
|
||||
router.onMessageReceived(rawJson: "{not valid json")
|
||||
router.onMessageReceived(rawJson: "{not valid json", isTrustedSource: true)
|
||||
|
||||
// Give async code a chance to run
|
||||
let expectation = expectation(description: "wait")
|
||||
@@ -149,6 +149,24 @@ final class MessageRouterTests: XCTestCase {
|
||||
XCTAssertEqual(sentCount, 0)
|
||||
}
|
||||
|
||||
func testUntrustedOriginMessagesAreDroppedBeforeDispatch() {
|
||||
var sentCount = 0
|
||||
let router = MessageRouter { _ in
|
||||
sentCount += 1
|
||||
}
|
||||
router.register(handler: StubHandler(domain: .secureStorage, result: ["value": "abc"]))
|
||||
|
||||
router.onMessageReceived(rawJson: makeRequestJSON(), isTrustedSource: false)
|
||||
|
||||
let expectation = expectation(description: "wait")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(sentCount, 0)
|
||||
}
|
||||
|
||||
// MARK: - Registration
|
||||
|
||||
func testRegisterReplacesExistingHandler() {
|
||||
@@ -162,7 +180,7 @@ final class MessageRouterTests: XCTestCase {
|
||||
router.register(handler: StubHandler(domain: .secureStorage, result: ["value": "first"]))
|
||||
router.register(handler: StubHandler(domain: .secureStorage, result: ["value": "second"]))
|
||||
|
||||
router.onMessageReceived(rawJson: makeRequestJSON())
|
||||
router.onMessageReceived(rawJson: makeRequestJSON(), isTrustedSource: true)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
import XCTest
|
||||
@testable import SelfNativeShell
|
||||
|
||||
final class RemoteContentIntegrityTests: XCTestCase {
|
||||
|
||||
// MARK: - normalizeSha256
|
||||
|
||||
func testStripsShaPrefixOnly() {
|
||||
XCTAssertEqual(
|
||||
RemoteContentIntegrity.normalizeSha256("sha256-abcdef1234567890"),
|
||||
"abcdef1234567890"
|
||||
)
|
||||
}
|
||||
|
||||
func testStrips0xPrefixOnly() {
|
||||
XCTAssertEqual(
|
||||
RemoteContentIntegrity.normalizeSha256("0xabcdef1234567890"),
|
||||
"abcdef1234567890"
|
||||
)
|
||||
}
|
||||
|
||||
func testStripsSha256Then0xPrefix() {
|
||||
XCTAssertEqual(
|
||||
RemoteContentIntegrity.normalizeSha256("sha256-0xabcdef"),
|
||||
"abcdef"
|
||||
)
|
||||
}
|
||||
|
||||
func testLowercasesInput() {
|
||||
XCTAssertEqual(
|
||||
RemoteContentIntegrity.normalizeSha256("ABCDEF"),
|
||||
"abcdef"
|
||||
)
|
||||
}
|
||||
|
||||
func testRawHexPassesThrough() {
|
||||
XCTAssertEqual(
|
||||
RemoteContentIntegrity.normalizeSha256("abcdef1234567890"),
|
||||
"abcdef1234567890"
|
||||
)
|
||||
}
|
||||
|
||||
func testDoesNotStripInteriorSha256() {
|
||||
XCTAssertEqual(
|
||||
RemoteContentIntegrity.normalizeSha256("absha256-cd"),
|
||||
"absha256-cd"
|
||||
)
|
||||
}
|
||||
|
||||
func testDoesNotStripInterior0x() {
|
||||
XCTAssertEqual(
|
||||
RemoteContentIntegrity.normalizeSha256("ab0xcd"),
|
||||
"ab0xcd"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isAcceptableMimeType
|
||||
|
||||
func testAcceptsNilMimeType() {
|
||||
XCTAssertTrue(RemoteContentIntegrity.isAcceptableMimeType(nil))
|
||||
}
|
||||
|
||||
func testAcceptsTextHtml() {
|
||||
XCTAssertTrue(RemoteContentIntegrity.isAcceptableMimeType("text/html"))
|
||||
}
|
||||
|
||||
func testRejectsApplicationJavascript() {
|
||||
XCTAssertFalse(RemoteContentIntegrity.isAcceptableMimeType("application/javascript"))
|
||||
}
|
||||
|
||||
func testRejectsApplicationJson() {
|
||||
XCTAssertFalse(RemoteContentIntegrity.isAcceptableMimeType("application/json"))
|
||||
}
|
||||
|
||||
func testRejectsTextPlain() {
|
||||
XCTAssertFalse(RemoteContentIntegrity.isAcceptableMimeType("text/plain"))
|
||||
}
|
||||
|
||||
func testRejectsEmptyString() {
|
||||
XCTAssertFalse(RemoteContentIntegrity.isAcceptableMimeType(""))
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,16 @@ final class RemoteNavigationPolicyTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testSubframeRejectsDiditOnNonStandardPort() {
|
||||
XCTAssertFalse(
|
||||
RemoteNavigationPolicy.isAllowedSubframeNavigation(
|
||||
url: URL(string: "https://verify.didit.me:8443/flow")!,
|
||||
remoteWebAppBaseURL: nil,
|
||||
isDebugMode: false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testMakeEntryURLAppendsHostedPathAndQuery() {
|
||||
XCTAssertEqual(
|
||||
RemoteNavigationPolicy.makeEntryURL(
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
import XCTest
|
||||
@testable import SelfNativeShell
|
||||
|
||||
final class SelfWebViewHostTests: XCTestCase {
|
||||
func testReleaseBuildUsesRemoteOrigin() throws {
|
||||
let router = MessageRouter(sendToWebView: { _ in })
|
||||
let host = SelfWebViewHost(router: router, isDebugMode: false)
|
||||
_ = host.createWebView()
|
||||
|
||||
let url = try XCTUnwrap(host.initialContentURL(queryParams: ""))
|
||||
XCTAssertEqual(url.scheme, "https")
|
||||
XCTAssertEqual(url.host, "self-app-alpha.vercel.app")
|
||||
XCTAssertTrue(url.path.contains("/tunnel/tour/1"))
|
||||
}
|
||||
|
||||
func testDebugBuildUsesLocalhost() throws {
|
||||
let router = MessageRouter(sendToWebView: { _ in })
|
||||
let host = SelfWebViewHost(router: router, isDebugMode: true)
|
||||
_ = host.createWebView()
|
||||
|
||||
let url = try XCTUnwrap(host.initialContentURL(queryParams: ""))
|
||||
XCTAssertEqual(url.scheme, "http")
|
||||
XCTAssertEqual(url.host, "localhost")
|
||||
XCTAssertTrue(url.absoluteString.hasPrefix("http://localhost:5173"))
|
||||
}
|
||||
|
||||
func testAllowedNavigationAcceptsRemoteAlphaOrigin() {
|
||||
let router = MessageRouter(sendToWebView: { _ in })
|
||||
let host = SelfWebViewHost(router: router, isDebugMode: false)
|
||||
_ = host.createWebView()
|
||||
|
||||
XCTAssertTrue(
|
||||
host.isAllowedNavigationURL(
|
||||
URL(string: "https://self-app-alpha.vercel.app/tunnel/tour/1")
|
||||
)
|
||||
)
|
||||
XCTAssertTrue(
|
||||
host.isAllowedNavigationURL(
|
||||
URL(string: "https://verify.didit.me/session/123")
|
||||
)
|
||||
)
|
||||
XCTAssertFalse(
|
||||
host.isAllowedNavigationURL(
|
||||
URL(string: "https://evil.example.com/tunnel/tour/1")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testHttpBaseURLProducesNilInRelease() {
|
||||
let router = MessageRouter(sendToWebView: { _ in })
|
||||
let host = SelfWebViewHost(
|
||||
router: router,
|
||||
isDebugMode: false,
|
||||
remoteWebAppBaseURL: URL(string: "http://self-app-alpha.vercel.app")
|
||||
)
|
||||
_ = host.createWebView()
|
||||
|
||||
XCTAssertNil(host.initialContentURL(queryParams: ""))
|
||||
}
|
||||
|
||||
func testBridgeTrustAcceptsRemoteRejectsDidit() {
|
||||
let router = MessageRouter(sendToWebView: { _ in })
|
||||
let host = SelfWebViewHost(router: router, isDebugMode: false)
|
||||
_ = host.createWebView()
|
||||
|
||||
XCTAssertTrue(
|
||||
host.isTrustedBridgeURL(
|
||||
URL(string: "https://self-app-alpha.vercel.app/tunnel/tour/1")
|
||||
)
|
||||
)
|
||||
XCTAssertFalse(
|
||||
host.isTrustedBridgeURL(
|
||||
URL(string: "https://verify.didit.me/session/123")
|
||||
)
|
||||
)
|
||||
XCTAssertFalse(
|
||||
host.isTrustedBridgeURL(
|
||||
URL(string: "https://evil.example.com/tunnel/tour/1")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -27,5 +27,10 @@ let package = Package(
|
||||
],
|
||||
path: "Sources/SelfSdkSwift"
|
||||
),
|
||||
.testTarget(
|
||||
name: "SelfSdkSwiftTests",
|
||||
dependencies: ["SelfSdkSwift"],
|
||||
path: "Tests/SelfSdkSwiftTests"
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -9,11 +9,17 @@ import WebKit
|
||||
/// Swift implementation of WebViewProvider using WKWebView.
|
||||
/// Handles message passing between the WebView and the KMP bridge.
|
||||
public class WebViewProviderImpl: NSObject {
|
||||
static let loopbackHost = "127.0.0.1"
|
||||
static let diditHost = "verify.didit.me"
|
||||
static let debugPort: UInt16 = 5173
|
||||
private static let defaultRemoteBaseURL = URL(string: "https://self-app-alpha.vercel.app")!
|
||||
|
||||
private var webView: WKWebView?
|
||||
private var viewController: UIViewController?
|
||||
private var onMessageReceived: ((String) -> Void)?
|
||||
private var isDebugMode: Bool = false
|
||||
private var remoteWebAppBaseURL: URL = WebViewProviderImpl.defaultRemoteBaseURL
|
||||
private var devServerUrl: String?
|
||||
|
||||
/// Weak proxy to avoid retain cycles with WKScriptMessageHandler
|
||||
private var messageProxy: WeakScriptMessageProxy?
|
||||
@@ -33,7 +39,7 @@ public class WebViewProviderImpl: NSObject {
|
||||
self.webView = nil
|
||||
self.viewController = nil
|
||||
}
|
||||
|
||||
|
||||
self.onMessageReceived = onMessageReceived
|
||||
|
||||
// Create message proxy to avoid retain cycle
|
||||
@@ -64,12 +70,8 @@ public class WebViewProviderImpl: NSObject {
|
||||
wv.navigationDelegate = self
|
||||
self.webView = wv
|
||||
|
||||
var urlString = "https://self-app-alpha.vercel.app/tunnel/tour/1"
|
||||
if let params = queryParams, !params.isEmpty {
|
||||
urlString += "?\(params)"
|
||||
}
|
||||
guard let url = URL(string: urlString) else {
|
||||
NSLog("SelfSDK-WebView: Failed to construct URL from: %@", urlString)
|
||||
guard let url = initialContentURL(queryParams: queryParams) else {
|
||||
NSLog("SelfSDK-WebView: Failed to construct bundled URL")
|
||||
return wv
|
||||
}
|
||||
wv.load(URLRequest(url: url))
|
||||
@@ -99,6 +101,21 @@ public class WebViewProviderImpl: NSObject {
|
||||
self.viewController = vc
|
||||
return vc
|
||||
}
|
||||
|
||||
@objc public func isBridgeRequestAllowed() -> Bool {
|
||||
isTrustedBridgeURL(webView?.url)
|
||||
}
|
||||
|
||||
@objc(configureRemoteLoadingRemoteWebAppBaseURL:)
|
||||
public func configureRemoteLoading(remoteWebAppBaseURL: String?) {
|
||||
self.remoteWebAppBaseURL = remoteWebAppBaseURL.flatMap { URL(string: $0) }
|
||||
?? Self.defaultRemoteBaseURL
|
||||
}
|
||||
|
||||
@objc(configureDevServerDevServerUrl:)
|
||||
public func configureDevServer(devServerUrl: String?) {
|
||||
self.devServerUrl = devServerUrl
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Host VC that embeds the WKWebView with proper Auto Layout
|
||||
@@ -145,10 +162,8 @@ extension WebViewProviderImpl: WKNavigationDelegate {
|
||||
decisionHandler(.cancel)
|
||||
return
|
||||
}
|
||||
let isTrusted =
|
||||
(url.scheme == "https" && host == "self-app-alpha.vercel.app") ||
|
||||
(isDebugMode && url.scheme == "http" && host == "127.0.0.1")
|
||||
decisionHandler(isTrusted ? .allow : .cancel)
|
||||
let isAllowed = isAllowedNavigationURL(url, host: host)
|
||||
decisionHandler(isAllowed ? .allow : .cancel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +174,10 @@ extension WebViewProviderImpl: WKScriptMessageHandler {
|
||||
_ userContentController: WKUserContentController,
|
||||
didReceive message: WKScriptMessage
|
||||
) {
|
||||
guard message.name == "SelfNativeIOS" else { return }
|
||||
guard message.name == "SelfNativeIOS",
|
||||
message.frameInfo.isMainFrame,
|
||||
isTrustedBridgeFrameInfo(message.frameInfo.securityOrigin),
|
||||
isBridgeRequestAllowed() else { return }
|
||||
|
||||
if let body = message.body as? String {
|
||||
onMessageReceived?(body)
|
||||
@@ -171,6 +189,100 @@ extension WebViewProviderImpl: WKScriptMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
extension WebViewProviderImpl {
|
||||
func initialContentURL(queryParams: String?) -> URL? {
|
||||
#if DEBUG
|
||||
if isDebugMode, let devUrl = devServerUrl, !devUrl.isEmpty,
|
||||
let baseURL = URL(string: devUrl.hasSuffix("/") ? String(devUrl.dropLast()) : devUrl) {
|
||||
var components = URLComponents()
|
||||
components.scheme = baseURL.scheme
|
||||
components.host = baseURL.host
|
||||
components.port = baseURL.port
|
||||
components.path = "/tunnel/tour/1"
|
||||
if let queryParams, !queryParams.isEmpty {
|
||||
components.percentEncodedQuery = queryParams
|
||||
}
|
||||
return components.url
|
||||
}
|
||||
|
||||
if isDebugMode {
|
||||
var components = URLComponents()
|
||||
components.scheme = "http"
|
||||
components.host = Self.loopbackHost
|
||||
components.port = Int(Self.debugPort)
|
||||
components.path = "/tunnel/tour/1"
|
||||
if let queryParams, !queryParams.isEmpty {
|
||||
components.percentEncodedQuery = queryParams
|
||||
}
|
||||
return components.url
|
||||
}
|
||||
#endif
|
||||
|
||||
guard remoteWebAppBaseURL.scheme == "https" else { return nil }
|
||||
var components = URLComponents()
|
||||
components.scheme = remoteWebAppBaseURL.scheme
|
||||
components.host = remoteWebAppBaseURL.host
|
||||
if let port = remoteWebAppBaseURL.port { components.port = port }
|
||||
components.path = "/tunnel/tour/1"
|
||||
if let queryParams, !queryParams.isEmpty {
|
||||
components.percentEncodedQuery = queryParams
|
||||
}
|
||||
return components.url
|
||||
}
|
||||
|
||||
func isAllowedNavigationURL(_ url: URL?, host: String? = nil) -> Bool {
|
||||
guard let url else { return false }
|
||||
let resolvedHost = host ?? url.host
|
||||
return isTrustedBridgeURL(url) ||
|
||||
(url.scheme == "https" && resolvedHost == Self.diditHost && resolvedPort(for: url) == 443)
|
||||
}
|
||||
|
||||
func isTrustedBridgeURL(_ url: URL?) -> Bool {
|
||||
guard let url else { return false }
|
||||
#if DEBUG
|
||||
if isDebugMode {
|
||||
if let devUrl = devServerUrl, !devUrl.isEmpty, let devBase = URL(string: devUrl) {
|
||||
return url.scheme == devBase.scheme && url.host == devBase.host && resolvedPort(for: url) == resolvedPort(for: devBase)
|
||||
}
|
||||
return url.scheme == "http" && url.host == Self.loopbackHost && url.port == Int(Self.debugPort)
|
||||
}
|
||||
#endif
|
||||
return url.scheme == remoteWebAppBaseURL.scheme &&
|
||||
url.host == remoteWebAppBaseURL.host &&
|
||||
resolvedPort(for: url) == resolvedPort(for: remoteWebAppBaseURL)
|
||||
}
|
||||
|
||||
func isTrustedBridgeFrameInfo(_ origin: WKSecurityOrigin) -> Bool {
|
||||
#if DEBUG
|
||||
if isDebugMode {
|
||||
if let devUrl = devServerUrl, !devUrl.isEmpty, let devBase = URL(string: devUrl) {
|
||||
let expectedPort = resolvedPort(for: devBase)
|
||||
return origin.protocol == devBase.scheme && origin.host == devBase.host && resolvedSecurityOriginPort(origin) == expectedPort
|
||||
}
|
||||
return origin.protocol == "http" && origin.host == Self.loopbackHost && origin.port == Int(Self.debugPort)
|
||||
}
|
||||
#endif
|
||||
let expectedPort = resolvedPort(for: remoteWebAppBaseURL)
|
||||
return origin.protocol == remoteWebAppBaseURL.scheme &&
|
||||
origin.host == remoteWebAppBaseURL.host &&
|
||||
resolvedSecurityOriginPort(origin) == expectedPort
|
||||
}
|
||||
|
||||
private func resolvedSecurityOriginPort(_ origin: WKSecurityOrigin) -> Int {
|
||||
if origin.port != 0 { return origin.port }
|
||||
switch origin.protocol {
|
||||
case "https": return 443
|
||||
case "http": return 80
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
private func resolvedPort(for url: URL) -> Int {
|
||||
if let port = url.port { return port }
|
||||
return url.scheme == "https" ? 443 : 80
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Weak proxy to break WKScriptMessageHandler retain cycle
|
||||
|
||||
/// WKUserContentController retains its message handler strongly.
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import XCTest
|
||||
@testable import SelfSdkSwift
|
||||
|
||||
final class WebViewProviderImplTests: XCTestCase {
|
||||
func testReleaseBuildUsesRemoteOrigin() throws {
|
||||
let provider = WebViewProviderImpl()
|
||||
|
||||
let url = try XCTUnwrap(provider.initialContentURL(queryParams: nil))
|
||||
XCTAssertEqual(url.scheme, "https")
|
||||
XCTAssertEqual(url.host, "self-app-alpha.vercel.app")
|
||||
XCTAssertTrue(url.path.contains("/tunnel/tour/1"))
|
||||
}
|
||||
|
||||
func testDebugBuildUsesLocalhost() throws {
|
||||
let provider = WebViewProviderImpl()
|
||||
_ = provider.createWebView(onMessageReceived: { _ in }, isDebugMode: true)
|
||||
|
||||
let url = try XCTUnwrap(provider.initialContentURL(queryParams: nil))
|
||||
XCTAssertEqual(url.scheme, "http")
|
||||
XCTAssertEqual(url.host, "127.0.0.1")
|
||||
XCTAssertEqual(url.port, 5173)
|
||||
XCTAssertTrue(url.path.contains("/tunnel/tour/1"))
|
||||
}
|
||||
|
||||
func testHttpBaseURLProducesNilInRelease() {
|
||||
let provider = WebViewProviderImpl()
|
||||
provider.configureRemoteLoading(remoteWebAppBaseURL: "http://self-app-alpha.vercel.app")
|
||||
|
||||
XCTAssertNil(provider.initialContentURL(queryParams: nil))
|
||||
}
|
||||
|
||||
func testAllowedNavigationAcceptsRemoteAlphaAndDidit() {
|
||||
let provider = WebViewProviderImpl()
|
||||
|
||||
XCTAssertTrue(
|
||||
provider.isAllowedNavigationURL(
|
||||
URL(string: "https://verify.didit.me/session/123")
|
||||
)
|
||||
)
|
||||
XCTAssertTrue(
|
||||
provider.isAllowedNavigationURL(
|
||||
URL(string: "https://self-app-alpha.vercel.app/tunnel/tour/1")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testDiditOnNonStandardPortIsRejected() {
|
||||
let provider = WebViewProviderImpl()
|
||||
|
||||
XCTAssertFalse(
|
||||
provider.isAllowedNavigationURL(
|
||||
URL(string: "https://verify.didit.me:8443/session/123")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testAllowedNavigationRejectsArbitraryOrigins() {
|
||||
let provider = WebViewProviderImpl()
|
||||
|
||||
XCTAssertFalse(
|
||||
provider.isAllowedNavigationURL(
|
||||
URL(string: "https://evil.com/tunnel/tour/1")
|
||||
)
|
||||
)
|
||||
XCTAssertFalse(
|
||||
provider.isAllowedNavigationURL(
|
||||
URL(string: "http://example.com/test")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testBridgeTrustAcceptsRemoteOrigin() {
|
||||
let provider = WebViewProviderImpl()
|
||||
|
||||
XCTAssertTrue(
|
||||
provider.isTrustedBridgeURL(
|
||||
URL(string: "https://self-app-alpha.vercel.app/tunnel/tour/1")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testBridgeTrustRejectsDiditAndArbitrary() {
|
||||
let provider = WebViewProviderImpl()
|
||||
|
||||
XCTAssertFalse(
|
||||
provider.isTrustedBridgeURL(
|
||||
URL(string: "https://verify.didit.me/session/123")
|
||||
)
|
||||
)
|
||||
XCTAssertFalse(
|
||||
provider.isTrustedBridgeURL(
|
||||
URL(string: "https://evil.com/tunnel/tour/1")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@
|
||||
"@didit-protocol/sdk-web": "^0.1.8",
|
||||
"@scure/bip32": "^2.0.1",
|
||||
"@scure/bip39": "^1.6.0",
|
||||
"@selfxyz/euclid": "1.4.0",
|
||||
"@selfxyz/euclid-core": "1.4.0",
|
||||
"@selfxyz/euclid": "1.4.2",
|
||||
"@selfxyz/euclid-core": "1.4.1",
|
||||
"@selfxyz/mobile-sdk-alpha": "workspace:^",
|
||||
"@selfxyz/webview-bridge": "workspace:^",
|
||||
"buffer": "^6.0.3",
|
||||
|
||||
@@ -47,13 +47,14 @@ import { VerificationResultScreen } from './screens/proving/VerificationResultSc
|
||||
import { BackupMethodPickerScreen } from './screens/recovery/BackupMethodPickerScreen';
|
||||
import { LaunchRecoveryScreen } from './screens/recovery/LaunchRecoveryScreen';
|
||||
import { RecoveryFailureScreen } from './screens/recovery/RecoveryFailureScreen';
|
||||
import { RecoveryPhraseScreen } from './screens/recovery/RecoveryPhraseScreen';
|
||||
import { OnboardingRecoveryPhraseScreen, RecoveryPhraseScreen } from './screens/recovery/RecoveryPhraseScreen';
|
||||
import { RecoverySuccessScreen } from './screens/recovery/RecoverySuccessScreen';
|
||||
import { SecretPhraseInputScreen } from './screens/recovery/SecretPhraseInputScreen';
|
||||
import { TourScreen as TunnelTourScreen } from './screens/tunnel/TourScreen';
|
||||
import { TunnelCountryPickerScreen } from './screens/tunnel/TunnelCountryPickerScreen';
|
||||
import { TunnelDiscloseScreen } from './screens/tunnel/TunnelDiscloseScreen';
|
||||
import { TunnelIDTypeScreen } from './screens/tunnel/TunnelIDTypeScreen';
|
||||
import { TunnelKycFailureScreen } from './screens/tunnel/TunnelKycFailureScreen';
|
||||
import { TunnelKycSuccessScreen } from './screens/tunnel/TunnelKycSuccessScreen';
|
||||
import { TunnelKycWrapper } from './screens/tunnel/TunnelKycWrapper';
|
||||
import { TunnelProofReceiptScreen } from './screens/tunnel/TunnelProofReceiptScreen';
|
||||
@@ -75,6 +76,7 @@ export const App: React.FC = () => (
|
||||
<Route path="/onboarding/provider-result" element={<ProviderResultScreen />} />
|
||||
<Route path="/onboarding/confirm" element={<ConfirmIdentificationScreen />} />
|
||||
<Route path="/onboarding/success" element={<ScanSuccessScreen />} />
|
||||
<Route path="/onboarding/recovery-phrase" element={<OnboardingRecoveryPhraseScreen />} />
|
||||
<Route path="/onboarding/failure" element={<RegistrationFailureScreen />} />
|
||||
<Route path="/onboarding/kyc-failure" element={<KycFailureScreen />} />
|
||||
<Route path="/proving" element={<ProvingScreen />} />
|
||||
@@ -110,6 +112,7 @@ export const App: React.FC = () => (
|
||||
<Route path="/coming-soon" element={<ComingSoonScreen />} />
|
||||
<Route path="/tunnel/tour/:step" element={<TunnelTourScreen />} />
|
||||
<Route path="/tunnel/kyc" element={<TunnelKycWrapper />} />
|
||||
<Route path="/tunnel/kyc-failure" element={<TunnelKycFailureScreen />} />
|
||||
<Route path="/tunnel/kyc-success" element={<TunnelKycSuccessScreen />} />
|
||||
<Route path="/tunnel/registration/country" element={<TunnelCountryPickerScreen />} />
|
||||
<Route path="/tunnel/registration/id-type" element={<TunnelIDTypeScreen />} />
|
||||
|
||||
@@ -14,6 +14,7 @@ interface DevScreenLink {
|
||||
interface DevScreenGroup {
|
||||
title: string;
|
||||
links: DevScreenLink[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const screenGroups: DevScreenGroup[] = [
|
||||
@@ -31,6 +32,7 @@ const screenGroups: DevScreenGroup[] = [
|
||||
{ href: '/onboarding/country', label: 'Country Picker' },
|
||||
{ href: '/onboarding/confirm', label: 'Confirm ID' },
|
||||
{ href: '/onboarding/success', label: 'Scan Success' },
|
||||
{ href: '/onboarding/recovery-phrase', label: 'Recovery Phrase' },
|
||||
{ href: '/onboarding/failure', label: 'Registration Failure' },
|
||||
{ href: '/onboarding/backup', label: 'Social Sign-On Method' },
|
||||
{ href: '/onboarding/signin', label: 'Social Sign-On' },
|
||||
@@ -72,15 +74,26 @@ const screenGroups: DevScreenGroup[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Tunnel',
|
||||
title: 'Tunnel — Screens',
|
||||
links: [
|
||||
{ href: '/tunnel/tour/1', label: 'Tour' },
|
||||
{ href: '/tunnel/kyc', label: 'KYC Mock' },
|
||||
{ href: '/tunnel/registration/country', label: 'Country Picker' },
|
||||
{ href: '/tunnel/registration/id-type', label: 'ID Type' },
|
||||
{ href: '/tunnel/proof/receipt', label: 'Proof Receipt' },
|
||||
{ href: '/tunnel/kyc-failure', label: 'KYC Failure' },
|
||||
{ href: '/tunnel/recovery-required', label: 'Recovery Required' },
|
||||
{ href: '/tunnel/proof/generating', label: 'Proving' },
|
||||
{ href: '/tunnel/proof/result', label: 'Result' },
|
||||
{ href: '/tunnel/proof/receipt', label: 'Proof Receipt' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Tunnel — Mock KYC',
|
||||
description: 'Mocks diverge after /tunnel/kyc; some outcomes intentionally share the same final route.',
|
||||
links: [
|
||||
{ href: '/tunnel/tour/1?mock=success', label: 'Flow → KYC Success, Then Proof Failure' },
|
||||
{ href: '/tunnel/tour/1?mock=kyc-failure', label: 'Flow → KYC Error (Retryable)' },
|
||||
{ href: '/tunnel/tour/1?mock=registration-failure', label: 'Flow → KYC Error (Fatal → Tour Step 4)' },
|
||||
{ href: '/tunnel/tour/1?mock=cancel', label: 'Flow → KYC Cancel → Tour Step 4' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -103,10 +116,10 @@ export const DevRouteMenu: React.FC = () => {
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const currentLabel = useMemo(
|
||||
() => allLinks.find(link => link.href === location.pathname)?.label ?? 'Dev Screens',
|
||||
[location.pathname],
|
||||
);
|
||||
const currentLabel = useMemo(() => {
|
||||
const fullPath = `${location.pathname}${location.search}`;
|
||||
return allLinks.find(link => link.href === fullPath)?.label ?? 'Dev Screens';
|
||||
}, [location.pathname, location.search]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -150,8 +163,20 @@ export const DevRouteMenu: React.FC = () => {
|
||||
>
|
||||
{group.title}
|
||||
</div>
|
||||
{group.description ? (
|
||||
<div
|
||||
style={{
|
||||
color: 'rgba(255, 255, 255, 0.65)',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.4,
|
||||
padding: '0 2px 6px',
|
||||
}}
|
||||
>
|
||||
{group.description}
|
||||
</div>
|
||||
) : null}
|
||||
{group.links.map(link => {
|
||||
const isActive = location.pathname === link.href;
|
||||
const isActive = `${location.pathname}${location.search}` === link.href;
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -11,7 +11,6 @@ import { ComingSoonScreen as EuclidComingSoonScreen } from '@selfxyz/euclid';
|
||||
import { useSelfClient } from '../providers/SelfClientProvider';
|
||||
import { getCountryName, renderFlag } from '../utils/countryFlags';
|
||||
import { WEB_SAFE_AREA } from '../utils/insets';
|
||||
import { shouldUseHistoryBack } from '../utils/mockOnboardingFlow';
|
||||
|
||||
export const ComingSoonScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -29,11 +28,6 @@ export const ComingSoonScreen: React.FC = () => {
|
||||
const onDismiss = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
analytics.trackEvent('coming_soon_dismissed');
|
||||
if (shouldUseHistoryBack()) {
|
||||
navigate(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate('/');
|
||||
}, [navigate, haptic, analytics]);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ConflictDetectedScreen as EuclidConflictDetectedScreen } from '@selfxyz
|
||||
|
||||
import { useSelfClient } from '../../providers/SelfClientProvider';
|
||||
import { WEB_SAFE_AREA } from '../../utils/insets';
|
||||
import { getPromptMockFromSearch, getPromptMockSearch, shouldUseHistoryBack } from '../../utils/mockOnboardingFlow';
|
||||
import { getPromptMockFromSearch, getPromptMockSearch } from '../../utils/mockOnboardingFlow';
|
||||
|
||||
export const ConflictDetectedScreen: React.FC = () => {
|
||||
const location = useLocation();
|
||||
@@ -32,11 +32,6 @@ export const ConflictDetectedScreen: React.FC = () => {
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
if (shouldUseHistoryBack()) {
|
||||
navigate(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(`/onboarding/signin${getPromptMockSearch(mock === 'existing-account' ? mock : 'default')}`);
|
||||
}, [mock, navigate, haptic]);
|
||||
|
||||
|
||||
@@ -15,12 +15,14 @@ import type { KycProviderResult } from '../../types/kycProvider';
|
||||
import { buildKycDocument } from '../../utils/buildKycDocument';
|
||||
import { waitForAttestation } from '../../utils/diditAttestation';
|
||||
import { createDiditSession, launchDiditWebSdk } from '../../utils/diditProvider';
|
||||
import { WEB_SAFE_AREA } from '../../utils/insets';
|
||||
|
||||
const CONTAINER_ID = 'didit-sdk-container';
|
||||
|
||||
type Phase = 'loading' | 'active' | 'waiting' | 'error';
|
||||
|
||||
interface ProviderLaunchState {
|
||||
backPath?: string;
|
||||
countryCode?: string;
|
||||
documentType?: string;
|
||||
nextPath?: string;
|
||||
@@ -32,9 +34,10 @@ export const ProviderLaunchScreen: React.FC = () => {
|
||||
const { client, analytics, haptic, lifecycle } = useSelfClient();
|
||||
const { verificationId: ctxVerificationId, environment } = useVerificationRequest();
|
||||
|
||||
const { countryCode = '', documentType = '', nextPath } = (location.state as ProviderLaunchState) || {};
|
||||
const { backPath, countryCode = '', documentType = '', nextPath } = (location.state as ProviderLaunchState) || {};
|
||||
|
||||
const defaultNextPath = nextPath ?? '/onboarding/provider-result';
|
||||
const isTunnelFlow = defaultNextPath.startsWith('/tunnel/') || backPath?.startsWith('/tunnel/') === true;
|
||||
const verificationId = ctxVerificationId ?? `didit-${Date.now()}`;
|
||||
|
||||
const [phase, setPhase] = useState<Phase>('loading');
|
||||
@@ -222,13 +225,20 @@ export const ProviderLaunchScreen: React.FC = () => {
|
||||
countryCode,
|
||||
documentType,
|
||||
});
|
||||
|
||||
if (isTunnelFlow) {
|
||||
navigate(backPath ?? '/tunnel/tour/4', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
lifecycle.dismiss({ reason: 'back' });
|
||||
if (window.history.length > 1) {
|
||||
navigate(-1);
|
||||
} else {
|
||||
navigate('/', { state: { skipOnboardingRedirect: true } });
|
||||
return;
|
||||
}
|
||||
}, [analytics, countryCode, documentType, haptic, lifecycle, navigate]);
|
||||
|
||||
navigate('/', { state: { skipOnboardingRedirect: true } });
|
||||
}, [analytics, backPath, countryCode, documentType, haptic, isTunnelFlow, lifecycle, navigate]);
|
||||
|
||||
const handleRetry = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
@@ -285,20 +295,33 @@ export const ProviderLaunchScreen: React.FC = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
backgroundColor: colors.white,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{phase === 'waiting' && (
|
||||
<KycPendingScreen
|
||||
insets={{ top: 0, bottom: 0 }}
|
||||
onCheckBackLater={handleBack}
|
||||
onReceiveLiveUpdates={() => {
|
||||
// TODO: wire up push notifications
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 1,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<KycPendingScreen
|
||||
insets={WEB_SAFE_AREA.insets}
|
||||
onCheckBackLater={handleBack}
|
||||
onReceiveLiveUpdates={() => {
|
||||
// TODO: wire up push notifications
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{phase === 'loading' && (
|
||||
<div
|
||||
@@ -327,18 +350,18 @@ export const ProviderLaunchScreen: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
<style>{`
|
||||
.shadow-card {
|
||||
#${CONTAINER_ID} .shadow-card {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
height: 100% !important;
|
||||
max-height: 100% !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
iframe[class*="in-iframe"] {
|
||||
#${CONTAINER_ID} iframe[class*="in-iframe"] {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
div[class*="size-full"] {
|
||||
#${CONTAINER_ID} div[class*="size-full"] {
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { PushNotificationPromptScreen as EuclidPushNotificationPromptScreen } fr
|
||||
|
||||
import { useSelfClient } from '../../providers/SelfClientProvider';
|
||||
import { WEB_SAFE_AREA } from '../../utils/insets';
|
||||
import { getPromptMockFromSearch, getPromptMockSearch, shouldUseHistoryBack } from '../../utils/mockOnboardingFlow';
|
||||
import { getPromptMockFromSearch, getPromptMockSearch } from '../../utils/mockOnboardingFlow';
|
||||
|
||||
export const PushNotificationPromptScreen: React.FC = () => {
|
||||
const location = useLocation();
|
||||
@@ -33,13 +33,7 @@ export const PushNotificationPromptScreen: React.FC = () => {
|
||||
const onClose = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
analytics.trackEvent('push_notification_header_back', { mock });
|
||||
|
||||
if (shouldUseHistoryBack()) {
|
||||
navigate(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(`/onboarding/backup${getPromptMockSearch(mock)}`);
|
||||
navigate(`/onboarding/recovery-phrase${getPromptMockSearch(mock)}`);
|
||||
}, [mock, navigate, haptic, analytics]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ScanSuccessScreen: React.FC = () => {
|
||||
const advanceToBackupPrompt = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
analytics.trackEvent('registration_success_finished');
|
||||
navigate(`/onboarding/backup${getPromptMockSearch()}`, {
|
||||
navigate(`/onboarding/recovery-phrase${getPromptMockSearch()}`, {
|
||||
state: { skipOnboardingRedirect: true },
|
||||
});
|
||||
}, [analytics, haptic, navigate]);
|
||||
|
||||
@@ -24,7 +24,7 @@ export const ProofSuccessBackupScreen: React.FC = () => {
|
||||
const onBackupAccount = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
analytics.trackEvent('proof_success_backup_pressed');
|
||||
navigate('/settings/security');
|
||||
navigate('/settings/recovery-phrase');
|
||||
}, [navigate, haptic, analytics]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { LaunchRecoveryScreen as EuclidLaunchRecoveryScreen, LeftArrowIcon } from '@selfxyz/euclid';
|
||||
|
||||
@@ -12,19 +12,26 @@ import { useSelfClient } from '../../providers/SelfClientProvider';
|
||||
import { WEB_SAFE_AREA } from '../../utils/insets';
|
||||
|
||||
export const LaunchRecoveryScreen: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { analytics, haptic } = useSelfClient();
|
||||
const backPath = (location.state as { backPath?: string } | null)?.backPath ?? '/settings/security';
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
navigate('/settings/security');
|
||||
}, [navigate, haptic]);
|
||||
navigate(backPath, { replace: true });
|
||||
}, [backPath, navigate, haptic]);
|
||||
|
||||
const isTunnelFlow = backPath.startsWith('/tunnel/');
|
||||
|
||||
const onEnterRecoveryPhrase = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
analytics.trackEvent('recovery_enter_phrase_pressed');
|
||||
navigate('/recovery/phrase-input');
|
||||
}, [navigate, haptic, analytics]);
|
||||
const target = isTunnelFlow
|
||||
? `/recovery/phrase-input?returnTo=${encodeURIComponent(backPath)}`
|
||||
: '/recovery/phrase-input';
|
||||
navigate(target);
|
||||
}, [backPath, isTunnelFlow, navigate, haptic, analytics]);
|
||||
|
||||
return (
|
||||
<div className="launch-recovery-screen">
|
||||
|
||||
@@ -4,55 +4,206 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import type { RecoveryPhraseVariant } from '@selfxyz/euclid';
|
||||
import { RecoveryPhraseScreen as EuclidRecoveryPhraseScreen } from '@selfxyz/euclid';
|
||||
import {
|
||||
borderRadius,
|
||||
colors,
|
||||
fontFamily,
|
||||
fontWeight,
|
||||
LeftArrowIcon,
|
||||
RecoveryPhrase,
|
||||
RecoveryPhraseScreen as EuclidRecoveryPhraseScreen,
|
||||
type RecoveryPhraseVariant,
|
||||
spacing,
|
||||
TopNavigationDialogue,
|
||||
} from '@selfxyz/euclid';
|
||||
import { bridgeStorageAdapter } from '@selfxyz/webview-bridge/adapters';
|
||||
|
||||
import { useBridge } from '../../providers/BridgeProvider';
|
||||
import { useSelfClient } from '../../providers/SelfClientProvider';
|
||||
import { WEB_SAFE_AREA } from '../../utils/insets';
|
||||
|
||||
const MNEMONIC_KEY = 'secret';
|
||||
import { getPromptMockFromSearch, getPromptMockSearch } from '../../utils/mockOnboardingFlow';
|
||||
import { ensureSecret, MNEMONIC_KEY } from '../../utils/secretManager';
|
||||
|
||||
function parseMnemonicWords(raw: string | null): string[] | undefined {
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(raw) as string | { phrase?: string };
|
||||
const phrase = typeof parsed === 'string' ? parsed : parsed.phrase;
|
||||
let phrase = raw;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as string | { phrase?: string };
|
||||
phrase = typeof parsed === 'string' ? parsed : (parsed.phrase ?? raw);
|
||||
} catch {
|
||||
phrase = raw;
|
||||
}
|
||||
|
||||
const words = phrase?.trim().split(/\s+/).filter(Boolean);
|
||||
|
||||
return words && words.length > 0 ? words : undefined;
|
||||
}
|
||||
|
||||
export const RecoveryPhraseScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const DEV_FAKE_MNEMONIC =
|
||||
'jump car stuff tiger camp core wasp dream harlem sales mistake wish expose moose dribble noodle tornado peanut install install meat snail truck virgo';
|
||||
|
||||
function getDevFallbackMnemonicWords(): string[] | undefined {
|
||||
if (!import.meta.env.DEV) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return DEV_FAKE_MNEMONIC.split(' ');
|
||||
}
|
||||
|
||||
async function resolveMnemonicWords(storage: ReturnType<typeof bridgeStorageAdapter>): Promise<string[] | undefined> {
|
||||
try {
|
||||
await ensureSecret(storage);
|
||||
const storedWords = parseMnemonicWords(await storage.get(MNEMONIC_KEY));
|
||||
return storedWords ?? getDevFallbackMnemonicWords();
|
||||
} catch {
|
||||
return getDevFallbackMnemonicWords();
|
||||
}
|
||||
}
|
||||
|
||||
interface RecoveryPhraseScreenBaseProps {
|
||||
onBack: () => void;
|
||||
onAppleBackup: () => void;
|
||||
onGoogleBackup: () => void;
|
||||
}
|
||||
|
||||
const recoveryPhrasePlaceholderWords = [
|
||||
'***#****',
|
||||
'****',
|
||||
'****&****',
|
||||
'(*****',
|
||||
'***#',
|
||||
'*****#********',
|
||||
'********',
|
||||
'**#**',
|
||||
'#*******',
|
||||
'******',
|
||||
'***',
|
||||
'*****#********',
|
||||
'**#***',
|
||||
'******',
|
||||
'******',
|
||||
'#**#',
|
||||
'********',
|
||||
'****',
|
||||
'!******',
|
||||
'*******',
|
||||
'***#*********',
|
||||
'*******',
|
||||
'******',
|
||||
'******',
|
||||
];
|
||||
|
||||
const copy = {
|
||||
navigationLabel: 'Recovery Phrase',
|
||||
infoTitle: 'Back up your account',
|
||||
infoDescriptionPrimary:
|
||||
'Your secret recovery phrase is used to restore your account if you lose your phone or need to reinstall the Self app.',
|
||||
infoDescriptionSecondary:
|
||||
'Save these 24 words in a secure location, such as a password manager, and never share them with anyone.',
|
||||
revealButtonLabel: 'Tap to reveal',
|
||||
};
|
||||
|
||||
const settingsStyles = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
flex: 1,
|
||||
backgroundColor: colors.slate50,
|
||||
},
|
||||
header: {
|
||||
backgroundColor: colors.slate50,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
overflowY: 'auto' as const,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingLeft: spacing.mdLg,
|
||||
paddingRight: spacing.mdLg,
|
||||
paddingBottom: spacing.xlLg,
|
||||
},
|
||||
content: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: spacing.mdLg,
|
||||
paddingTop: spacing.mdLg,
|
||||
},
|
||||
infoBox: {
|
||||
backgroundColor: colors.blue50,
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid' as const,
|
||||
borderColor: colors.blue100,
|
||||
borderRadius: borderRadius.mdd,
|
||||
overflow: 'hidden' as const,
|
||||
},
|
||||
infoBoxContent: {
|
||||
padding: spacing.mdLg,
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: spacing.mdLg,
|
||||
},
|
||||
infoTextContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: spacing.smLg,
|
||||
},
|
||||
infoTitle: {
|
||||
fontFamily: fontFamily.dinOT,
|
||||
fontWeight: fontWeight.medium,
|
||||
fontSize: 18,
|
||||
color: colors.black,
|
||||
lineHeight: '22px',
|
||||
},
|
||||
infoDescriptionContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: spacing.smPlus,
|
||||
},
|
||||
infoDescription: {
|
||||
fontFamily: fontFamily.dinOT,
|
||||
fontWeight: fontWeight.medium,
|
||||
fontSize: 14,
|
||||
color: colors.slate500,
|
||||
lineHeight: '20px',
|
||||
},
|
||||
recoveryPhraseContainer: {
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const RecoveryPhraseScreenBase: React.FC<RecoveryPhraseScreenBaseProps> = ({
|
||||
onBack,
|
||||
onAppleBackup,
|
||||
onGoogleBackup,
|
||||
}) => {
|
||||
const bridge = useBridge();
|
||||
const storage = useRef(bridgeStorageAdapter(bridge)).current;
|
||||
const { analytics, haptic } = useSelfClient();
|
||||
const [variant, setVariant] = useState<RecoveryPhraseVariant>('hidden');
|
||||
const [words, setWords] = useState<string[] | undefined>();
|
||||
|
||||
const onBack = useCallback(() => {
|
||||
const handleBack = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
navigate(-1);
|
||||
}, [navigate, haptic]);
|
||||
onBack();
|
||||
}, [haptic, onBack]);
|
||||
|
||||
const onReveal = useCallback(async () => {
|
||||
haptic.trigger('selection');
|
||||
analytics.trackEvent('recovery_phrase_revealed');
|
||||
|
||||
let resolvedWords: string[] | undefined;
|
||||
|
||||
try {
|
||||
resolvedWords = parseMnemonicWords(await storage.get(MNEMONIC_KEY));
|
||||
} catch {
|
||||
// Storage or parsing failed — words stay undefined, Euclid shows placeholders.
|
||||
const resolvedWords = await resolveMnemonicWords(storage);
|
||||
if (!resolvedWords?.length) {
|
||||
haptic.trigger('error');
|
||||
return;
|
||||
}
|
||||
|
||||
setWords(resolvedWords);
|
||||
setVariant('revealed');
|
||||
}, [haptic, analytics, storage]);
|
||||
@@ -78,11 +229,124 @@ export const RecoveryPhraseScreen: React.FC = () => {
|
||||
insets={WEB_SAFE_AREA.insets}
|
||||
words={words}
|
||||
variant={variant}
|
||||
onBack={onBack}
|
||||
onBack={handleBack}
|
||||
onReveal={onReveal}
|
||||
onCopy={onCopy}
|
||||
onAppleBackup={() => navigate('/coming-soon')}
|
||||
onGoogleBackup={() => navigate('/coming-soon')}
|
||||
onAppleBackup={onAppleBackup}
|
||||
onGoogleBackup={onGoogleBackup}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SettingsRecoveryPhraseScreen: React.FC<{ onBack: () => void }> = ({ onBack }) => {
|
||||
const bridge = useBridge();
|
||||
const storage = useRef(bridgeStorageAdapter(bridge)).current;
|
||||
const { analytics, haptic } = useSelfClient();
|
||||
const [variant, setVariant] = useState<RecoveryPhraseVariant>('hidden');
|
||||
const [words, setWords] = useState<string[] | undefined>();
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
onBack();
|
||||
}, [haptic, onBack]);
|
||||
|
||||
const onReveal = useCallback(async () => {
|
||||
haptic.trigger('selection');
|
||||
analytics.trackEvent('recovery_phrase_revealed');
|
||||
|
||||
const resolvedWords = await resolveMnemonicWords(storage);
|
||||
if (!resolvedWords?.length) {
|
||||
haptic.trigger('error');
|
||||
return;
|
||||
}
|
||||
setWords(resolvedWords);
|
||||
setVariant('revealed');
|
||||
}, [analytics, haptic, storage]);
|
||||
|
||||
const onCopy = useCallback(async () => {
|
||||
analytics.trackEvent('recovery_phrase_copied');
|
||||
|
||||
if (!words?.length || !navigator.clipboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(words.join(' '));
|
||||
haptic.trigger('success');
|
||||
setVariant('copied');
|
||||
} catch {
|
||||
haptic.trigger('error');
|
||||
}
|
||||
}, [analytics, haptic, words]);
|
||||
|
||||
return (
|
||||
<div style={settingsStyles.container}>
|
||||
<div style={settingsStyles.header}>
|
||||
<TopNavigationDialogue
|
||||
variant="Primary"
|
||||
label={copy.navigationLabel}
|
||||
escapeIcon={({ size, color }) => <LeftArrowIcon size={size} color={color} />}
|
||||
infoIcon={({ size }) => <div style={{ width: size, height: size }} />}
|
||||
onEscape={handleBack}
|
||||
onPressInfo={() => {}}
|
||||
/>
|
||||
</div>
|
||||
<div style={settingsStyles.scrollView}>
|
||||
<div style={settingsStyles.scrollContent}>
|
||||
<div style={settingsStyles.content}>
|
||||
<div style={settingsStyles.infoBox}>
|
||||
<div style={settingsStyles.infoBoxContent}>
|
||||
<div style={settingsStyles.infoTextContainer}>
|
||||
<span style={settingsStyles.infoTitle}>{copy.infoTitle}</span>
|
||||
<div style={settingsStyles.infoDescriptionContainer}>
|
||||
<span style={settingsStyles.infoDescription}>{copy.infoDescriptionPrimary}</span>
|
||||
<span style={settingsStyles.infoDescription}>{copy.infoDescriptionSecondary}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={settingsStyles.recoveryPhraseContainer}>
|
||||
<RecoveryPhrase
|
||||
variant={variant}
|
||||
words={words || recoveryPhrasePlaceholderWords}
|
||||
onReveal={onReveal}
|
||||
onCopy={onCopy}
|
||||
revealButtonText={copy.revealButtonLabel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const OnboardingRecoveryPhraseScreen: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const mock = getPromptMockFromSearch(location.search);
|
||||
const notificationsPath = `/onboarding/notifications${getPromptMockSearch(mock)}`;
|
||||
const successPath = `/onboarding/success${getPromptMockSearch(mock)}`;
|
||||
|
||||
const onBack = useCallback(() => {
|
||||
navigate(successPath);
|
||||
}, [navigate, successPath]);
|
||||
|
||||
const advanceToNotifications = useCallback(() => {
|
||||
navigate(notificationsPath);
|
||||
}, [navigate, notificationsPath]);
|
||||
|
||||
return (
|
||||
<RecoveryPhraseScreenBase
|
||||
onBack={onBack}
|
||||
onAppleBackup={advanceToNotifications}
|
||||
onGoogleBackup={advanceToNotifications}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RecoveryPhraseScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return <SettingsRecoveryPhraseScreen onBack={() => navigate(-1)} />;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Navigate, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { LaunchTour1Screen, LaunchTour2Screen, LaunchTour3Screen, LaunchTour4Screen } from '@selfxyz/euclid';
|
||||
import { loadSelectedDocument } from '@selfxyz/mobile-sdk-alpha/browser';
|
||||
@@ -14,13 +14,15 @@ import { WEB_SAFE_AREA } from '../../utils/insets';
|
||||
|
||||
export const TourScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { step } = useParams<{ step: string }>();
|
||||
const stepNum = parseInt(step ?? '1', 10);
|
||||
const { client } = useSelfClient();
|
||||
const mockParam = import.meta.env.DEV ? location.search : '';
|
||||
|
||||
const onNext = useCallback(async () => {
|
||||
if (stepNum < 4) {
|
||||
navigate(`/tunnel/tour/${stepNum + 1}`);
|
||||
navigate(`/tunnel/tour/${stepNum + 1}${mockParam}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,12 +36,12 @@ export const TourScreen: React.FC = () => {
|
||||
// Fall through to KYC when document state is unavailable.
|
||||
}
|
||||
|
||||
navigate('/tunnel/kyc');
|
||||
}, [navigate, stepNum, client]);
|
||||
navigate(`/tunnel/kyc${mockParam}`);
|
||||
}, [navigate, stepNum, client, mockParam]);
|
||||
|
||||
const onRestore = useCallback(() => {
|
||||
navigate('/recovery');
|
||||
}, []);
|
||||
navigate('/recovery', { state: { backPath: `/tunnel/tour/${step ?? '1'}` } });
|
||||
}, [navigate, step]);
|
||||
|
||||
switch (step) {
|
||||
case '1':
|
||||
|
||||
@@ -51,7 +51,7 @@ export const TunnelIDTypeScreen: React.FC = () => {
|
||||
countryName={getCountryName(countryCode)}
|
||||
idTypes={idTypes}
|
||||
onIDTypeSelect={onIDTypeSelect}
|
||||
onBack={() => navigate(-1)}
|
||||
onBack={() => navigate('/tunnel/registration/country')}
|
||||
renderFlag={renderFlag}
|
||||
renderIDTypeIcon={renderIDTypeIcon}
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user