mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
Merge pull request #1620 from selfxyz/release/staging-2026-01-16
Release to Staging - 2026-01-16
This commit is contained in:
@@ -20,9 +20,9 @@
|
||||
## Core Components
|
||||
|
||||
1. Identity Verification Hub
|
||||
- Manages multi-step verification process for passports and EU ID cards
|
||||
- Manages multi-step verification process for passports, EU ID cards, Aadhaar, and Selfrica ID cards
|
||||
- Handles document attestation through zero-knowledge proofs
|
||||
- Implements verification paths: E-PASSPORT and EU_ID_CARD
|
||||
- Implements verification paths: E-PASSPORT, EU_ID_CARD, AADHAAR, and SELFRICA_ID_CARD
|
||||
- File: contracts/contracts/IdentityVerificationHubImplV2.sol
|
||||
|
||||
2. Document Verification Processing
|
||||
@@ -40,10 +40,10 @@
|
||||
- Files: noir/crates/dg1/src/ofac/*.nr
|
||||
|
||||
4. Identity Registry Management
|
||||
- Maintains separate registries for passports and ID cards
|
||||
- Maintains separate registries for passports, EU ID cards, Aadhaar, and Selfrica
|
||||
- Handles DSC key commitment registration
|
||||
- Implements nullifier tracking for duplicate prevention
|
||||
- File: contracts/contracts/registry/IdentityRegistryImplV1.sol
|
||||
- Files: contracts/contracts/registry/IdentityRegistryImplV1.sol, IdentityRegistryIdCardImplV1.sol, IdentityRegistryAadhaarImplV1.sol, IdentityRegistrySelfricaImplV1.sol
|
||||
|
||||
## Core Workflows
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
8bc1e85075f73906767652ab35d5563efce2a931:packages/mobile-sdk-alpha/src/animations/passport_verify.json:aws-access-token:6
|
||||
0e4555eee6589aa9cca68f451227b149277d8c90:app/tests/src/utils/points/api.test.ts:generic-api-key:34
|
||||
circuits/circuits/gcp_jwt_verifier/example_jwt.txt:jwt:1
|
||||
circuits/circuits/gcp_jwt_verifier/example_jwt_fail.txt:jwt:1
|
||||
cadd7ae5b768c261230f84426eac879c1853ce70:app/ios/Podfile.lock:generic-api-key:2586
|
||||
aeb8287078f088ecd8e9430e3f6a9f2c593ef1fc:app/src/utils/points/constants.ts:generic-api-key:7
|
||||
app/src/services/points/constants.ts:generic-api-key:10
|
||||
|
||||
@@ -23,7 +23,7 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1201.0)
|
||||
aws-partitions (1.1204.0)
|
||||
aws-sdk-core (3.241.3)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
@@ -254,7 +254,7 @@ GEM
|
||||
optparse (0.8.1)
|
||||
os (1.1.4)
|
||||
plist (3.7.2)
|
||||
prism (1.7.0)
|
||||
prism (1.8.0)
|
||||
public_suffix (4.0.7)
|
||||
racc (1.8.1)
|
||||
rake (13.3.1)
|
||||
|
||||
@@ -134,8 +134,8 @@ android {
|
||||
applicationId "com.proofofpassportapp"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 121
|
||||
versionName "2.9.11"
|
||||
versionCode 136
|
||||
versionName "2.9.13"
|
||||
manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp']
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
@@ -231,8 +231,9 @@ dependencies {
|
||||
implementation "com.google.guava:guava:31.1-android"
|
||||
implementation "androidx.profileinstaller:profileinstaller:1.3.1"
|
||||
|
||||
implementation "androidx.activity:activity:1.9.3"
|
||||
implementation "androidx.activity:activity-ktx:1.9.3"
|
||||
implementation "androidx.activity:activity:1.10.1"
|
||||
implementation "androidx.activity:activity-ktx:1.10.1"
|
||||
implementation "com.google.android.material:material:1.12.0"
|
||||
|
||||
implementation "com.google.android.play:app-update:2.1.0"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:extractNativeLibs="false"
|
||||
tools:replace="android:icon, android:roundIcon, android:name, android:extractNativeLibs"
|
||||
android:allowBackup="false"
|
||||
tools:replace="android:icon, android:roundIcon, android:name, android:extractNativeLibs, android:allowBackup"
|
||||
android:theme="@style/AppTheme"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="false"
|
||||
|
||||
@@ -5,9 +5,8 @@ package com.proofofpassportapp
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.graphics.Color
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
@@ -44,12 +43,11 @@ class MainActivity : ReactActivity() {
|
||||
// Prevent fragment state restoration to avoid react-native-screens crash
|
||||
// See: https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704978
|
||||
super.onCreate(null)
|
||||
// Ensure edge-to-edge is enabled consistently across Android versions using
|
||||
// the AndroidX helper so deprecated window color APIs are avoided.
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||
navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT)
|
||||
)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
WindowInsetsControllerCompat(window, window.decorView).apply {
|
||||
systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
// Allow system to manage orientation for large screens
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.9.11</string>
|
||||
<string>2.9.13</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -2065,7 +2065,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNScreens (4.15.3):
|
||||
- RNScreens (4.17.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2086,9 +2086,9 @@ PODS:
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- RNScreens/common (= 4.15.3)
|
||||
- RNScreens/common (= 4.17.0)
|
||||
- Yoga
|
||||
- RNScreens/common (4.15.3):
|
||||
- RNScreens/common (4.17.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2639,7 +2639,7 @@ SPEC CHECKSUMS:
|
||||
RNKeychain: 471ceef8c13f15a5534c3cd2674dbbd9d0680e52
|
||||
RNLocalize: 4f5e4a46d2bccd04ccb96721e438dcb9de17c2e0
|
||||
RNReactNativeHapticFeedback: e526ac4a7ca9fb23c7843ea4fd7d823166054c73
|
||||
RNScreens: 806e1449a8ec63c2a4e4cf8a63cc80203ccda9b8
|
||||
RNScreens: 8049d2198d60c2b8f00b115270ab7a08edfa4190
|
||||
RNSentry: 6ad982be2c8e32dab912afb4132b6a0d88484ea0
|
||||
RNSVG: e1cf5a9a5aa12c69f2ec47031defbd87ae7fb697
|
||||
segment-analytics-react-native: 0eae155b0e9fa560fa6b17d78941df64537c35b7
|
||||
|
||||
@@ -546,7 +546,7 @@
|
||||
"$(PROJECT_DIR)",
|
||||
"$(PROJECT_DIR)/MoproKit/Libs",
|
||||
);
|
||||
MARKETING_VERSION = 2.9.11;
|
||||
MARKETING_VERSION = 2.9.13;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -686,7 +686,7 @@
|
||||
"$(PROJECT_DIR)",
|
||||
"$(PROJECT_DIR)/MoproKit/Libs",
|
||||
);
|
||||
MARKETING_VERSION = 2.9.11;
|
||||
MARKETING_VERSION = 2.9.13;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@selfxyz/mobile-app",
|
||||
"version": "2.9.11",
|
||||
"version": "2.9.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -161,7 +161,7 @@
|
||||
"react-native-passkey": "^3.3.1",
|
||||
"react-native-passport-reader": "1.0.3",
|
||||
"react-native-safe-area-context": "^5.6.1",
|
||||
"react-native-screens": "4.15.3",
|
||||
"react-native-screens": "4.17.0",
|
||||
"react-native-sqlite-storage": "^6.0.1",
|
||||
"react-native-svg": "15.14.0",
|
||||
"react-native-svg-web": "1.0.9",
|
||||
|
||||
@@ -30,7 +30,7 @@ const ModalBackDrop = styled(View, {
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
export interface FeedbackModalScreenParams {
|
||||
export interface AlertModalParams {
|
||||
titleText: string;
|
||||
bodyText: string;
|
||||
buttonText: string;
|
||||
@@ -41,13 +41,13 @@ export interface FeedbackModalScreenParams {
|
||||
preventDismiss?: boolean;
|
||||
}
|
||||
|
||||
interface FeedbackModalScreenProps {
|
||||
interface AlertModalProps {
|
||||
visible: boolean;
|
||||
modalParams: FeedbackModalScreenParams | null;
|
||||
modalParams: AlertModalParams | null;
|
||||
onHideModal?: () => void;
|
||||
}
|
||||
|
||||
const FeedbackModalScreen: React.FC<FeedbackModalScreenProps> = ({
|
||||
const AlertModal: React.FC<AlertModalProps> = ({
|
||||
visible,
|
||||
modalParams,
|
||||
onHideModal,
|
||||
@@ -145,4 +145,4 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
export default FeedbackModalScreen;
|
||||
export default AlertModal;
|
||||
@@ -2,24 +2,25 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Alert, Modal, StyleSheet, Text, TextInput, View } from 'react-native';
|
||||
import React from 'react';
|
||||
import { Modal, StyleSheet, Text, View } from 'react-native';
|
||||
import { Button, XStack, YStack } from 'tamagui';
|
||||
|
||||
import { Caption } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import {
|
||||
black,
|
||||
slate400,
|
||||
white,
|
||||
zinc800,
|
||||
zinc900,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { advercase, dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import ModalClose from '@/assets/icons/modal_close.svg';
|
||||
import { openSupportForm } from '@/services/support';
|
||||
|
||||
interface FeedbackModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (
|
||||
onSubmit?: (
|
||||
feedback: string,
|
||||
category: string,
|
||||
name?: string,
|
||||
@@ -27,65 +28,10 @@ interface FeedbackModalProps {
|
||||
) => void;
|
||||
}
|
||||
|
||||
const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const [category, setCategory] = useState('general');
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const categories = [
|
||||
{ value: 'general', label: 'General Feedback' },
|
||||
{ value: 'bug', label: 'Bug Report' },
|
||||
{ value: 'feature', label: 'Feature Request' },
|
||||
{ value: 'ui', label: 'UI/UX Issue' },
|
||||
];
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!feedback.trim()) {
|
||||
Alert.alert('Error', 'Please enter your feedback');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await onSubmit(
|
||||
feedback.trim(),
|
||||
category,
|
||||
name.trim() || undefined,
|
||||
email.trim() || undefined,
|
||||
);
|
||||
setFeedback('');
|
||||
setCategory('general');
|
||||
setName('');
|
||||
setEmail('');
|
||||
onClose();
|
||||
Alert.alert('Success', 'Thank you for your feedback!');
|
||||
} catch (error) {
|
||||
console.error('Error submitting feedback:', error);
|
||||
Alert.alert('Error', 'Failed to submit feedback. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (feedback.trim() || name.trim() || email.trim()) {
|
||||
Alert.alert(
|
||||
'Discard Feedback?',
|
||||
'You have unsaved feedback. Are you sure you want to close?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{ text: 'Discard', style: 'destructive', onPress: onClose },
|
||||
],
|
||||
);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
const FeedbackModal: React.FC<FeedbackModalProps> = ({ visible, onClose }) => {
|
||||
const handleSupportForm = async () => {
|
||||
await openSupportForm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -93,93 +39,33 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
||||
visible={visible}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={handleClose}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.modalContainer}>
|
||||
<YStack gap="$4" padding="$4">
|
||||
<XStack justifyContent="space-between" alignItems="center">
|
||||
<Text style={styles.title}>Send Feedback</Text>
|
||||
<Button
|
||||
size="$2"
|
||||
variant="outlined"
|
||||
onPress={handleClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
<ModalClose onPress={onClose} />
|
||||
</XStack>
|
||||
|
||||
<YStack gap="$2">
|
||||
<Caption style={styles.label}>Category</Caption>
|
||||
<XStack gap="$2" flexWrap="wrap">
|
||||
{categories.map(cat => (
|
||||
<Button
|
||||
key={cat.value}
|
||||
size="$2"
|
||||
backgroundColor={
|
||||
category === cat.value ? white : 'transparent'
|
||||
}
|
||||
color={category === cat.value ? black : white}
|
||||
borderColor={white}
|
||||
onPress={() => setCategory(cat.value)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{cat.label}
|
||||
</Button>
|
||||
))}
|
||||
</XStack>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$2">
|
||||
<Caption style={styles.label}>
|
||||
Contact Information (Optional)
|
||||
<YStack gap="$3" alignItems="center" paddingVertical="$2">
|
||||
<Caption style={styles.messageText}>
|
||||
Have feedback, suggestions, or found a bug?
|
||||
</Caption>
|
||||
<Caption style={styles.messageText}>
|
||||
Fill out our feedback form and we'll review it as soon as
|
||||
possible.
|
||||
</Caption>
|
||||
<XStack gap="$2">
|
||||
<TextInput
|
||||
style={[styles.textInput, { flex: 1, minHeight: 48 }]}
|
||||
placeholder="Name"
|
||||
placeholderTextColor={slate400}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
editable={!isSubmitting}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.textInput, { flex: 1, minHeight: 48 }]}
|
||||
placeholder="Email"
|
||||
placeholderTextColor={slate400}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
editable={!isSubmitting}
|
||||
/>
|
||||
</XStack>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$2">
|
||||
<Caption style={styles.label}>Your Feedback</Caption>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
placeholder="Tell us what you think, report a bug, or suggest a feature..."
|
||||
placeholderTextColor={slate400}
|
||||
value={feedback}
|
||||
onChangeText={setFeedback}
|
||||
multiline
|
||||
numberOfLines={6}
|
||||
textAlignVertical="top"
|
||||
editable={!isSubmitting}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
<Button
|
||||
size="$4"
|
||||
backgroundColor={white}
|
||||
color={black}
|
||||
onPress={handleSubmit}
|
||||
disabled={isSubmitting || !feedback.trim()}
|
||||
color="$black"
|
||||
onPress={handleSupportForm}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit Feedback'}
|
||||
Open Feedback Form
|
||||
</Button>
|
||||
</YStack>
|
||||
</View>
|
||||
@@ -201,7 +87,6 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 16,
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
maxHeight: '80%',
|
||||
borderWidth: 1,
|
||||
borderColor: zinc800,
|
||||
},
|
||||
@@ -211,22 +96,12 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '600',
|
||||
color: white,
|
||||
},
|
||||
label: {
|
||||
messageText: {
|
||||
fontFamily: dinot,
|
||||
color: white,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
textInput: {
|
||||
backgroundColor: black,
|
||||
borderWidth: 1,
|
||||
borderColor: zinc800,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
color: white,
|
||||
fontSize: 16,
|
||||
fontFamily: dinot,
|
||||
minHeight: 120,
|
||||
fontSize: 15,
|
||||
textAlign: 'center',
|
||||
lineHeight: 22,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
19
app/src/components/SystemBars.tsx
Normal file
19
app/src/components/SystemBars.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import { SystemBars as EdgeToEdgeSystemBars } from 'react-native-edge-to-edge';
|
||||
|
||||
export type { SystemBarStyle } from 'react-native-edge-to-edge';
|
||||
|
||||
type SystemBarsProps = React.ComponentProps<typeof EdgeToEdgeSystemBars>;
|
||||
|
||||
export const SystemBars: React.FC<SystemBarsProps> = props => {
|
||||
if (Platform.OS === 'android' || Platform.OS === 'web') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <EdgeToEdgeSystemBars {...props} />;
|
||||
};
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import type { TextProps } from 'react-native';
|
||||
import type { SystemBarStyle } from 'react-native-edge-to-edge';
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import { ChevronLeft, X } from '@tamagui/lucide-icons';
|
||||
|
||||
import type { ViewProps } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
@@ -16,6 +14,9 @@ import {
|
||||
XStack,
|
||||
} from '@selfxyz/mobile-sdk-alpha/components';
|
||||
|
||||
import type { SystemBarStyle } from '@/components/SystemBars';
|
||||
import { SystemBars } from '@/components/SystemBars';
|
||||
|
||||
interface NavBarProps extends ViewProps {
|
||||
children: React.ReactNode;
|
||||
backgroundColor?: string;
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import type { NativeStackHeaderProps } from '@react-navigation/native-stack';
|
||||
|
||||
import { SystemBars } from '@/components/SystemBars';
|
||||
|
||||
export const HeadlessNavForEuclid = (props: NativeStackHeaderProps) => {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -33,6 +33,8 @@ export const referralBaseUrl = 'https://referral.self.xyz';
|
||||
export const selfLogoReverseUrl =
|
||||
'https://storage.googleapis.com/self-logo-reverse/Self%20Logomark%20Reverse.png';
|
||||
export const selfUrl = 'https://self.xyz';
|
||||
export const supportFormUrl =
|
||||
'https://hail-jonquil-ef8.notion.site/2b057801cd128041985dfd6e1722eca1';
|
||||
export const supportedBiometricIdsUrl =
|
||||
'https://docs.self.xyz/use-self/self-map-countries-list';
|
||||
export const telegramUrl = 'https://t.me/selfxyz';
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
showFeedbackWidget,
|
||||
} from '@sentry/react-native';
|
||||
|
||||
import type { FeedbackModalScreenParams } from '@/components/FeedbackModalScreen';
|
||||
import type { AlertModalParams } from '@/components/AlertModal';
|
||||
import { captureFeedback } from '@/config/sentry';
|
||||
|
||||
export type FeedbackType = 'button' | 'widget' | 'custom';
|
||||
@@ -18,8 +18,7 @@ export const useFeedbackModal = () => {
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [modalParams, setModalParams] =
|
||||
useState<FeedbackModalScreenParams | null>(null);
|
||||
const [modalParams, setModalParams] = useState<AlertModalParams | null>(null);
|
||||
|
||||
const showFeedbackModal = useCallback((type: FeedbackType = 'button') => {
|
||||
if (timeoutRef.current) {
|
||||
@@ -81,7 +80,7 @@ export const useFeedbackModal = () => {
|
||||
setIsVisible(false);
|
||||
}, []);
|
||||
|
||||
const showModal = useCallback((params: FeedbackModalScreenParams) => {
|
||||
const showModal = useCallback((params: AlertModalParams) => {
|
||||
setModalParams(params);
|
||||
setIsModalVisible(true);
|
||||
}, []);
|
||||
|
||||
@@ -7,10 +7,12 @@ import type {
|
||||
ACCESSIBLE,
|
||||
GetOptions,
|
||||
SECURITY_LEVEL,
|
||||
SetOptions,
|
||||
} from 'react-native-keychain';
|
||||
import Keychain from 'react-native-keychain';
|
||||
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import type { ExtendedSetOptions } from '@/types/react-native-keychain';
|
||||
|
||||
/**
|
||||
* Security configuration for keychain operations
|
||||
*/
|
||||
@@ -23,6 +25,8 @@ export interface AdaptiveSecurityConfig {
|
||||
export interface GetSecureOptions {
|
||||
requireAuth?: boolean;
|
||||
promptMessage?: string;
|
||||
/** Whether to use StrongBox-backed key generation on Android. Default: true */
|
||||
useStrongBox?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,7 +65,8 @@ export async function checkPasscodeAvailable(): Promise<boolean> {
|
||||
await Keychain.setGenericPassword('test', 'test', {
|
||||
service: testService,
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY,
|
||||
});
|
||||
useStrongBox: false,
|
||||
} as ExtendedSetOptions);
|
||||
// Clean up test entry
|
||||
await Keychain.resetGenericPassword({ service: testService });
|
||||
return true;
|
||||
@@ -78,7 +83,7 @@ export async function createKeychainOptions(
|
||||
options: GetSecureOptions,
|
||||
capabilities?: SecurityCapabilities,
|
||||
): Promise<{
|
||||
setOptions: SetOptions;
|
||||
setOptions: ExtendedSetOptions;
|
||||
getOptions: GetOptions;
|
||||
}> {
|
||||
const config = await getAdaptiveSecurityConfig(
|
||||
@@ -86,10 +91,14 @@ export async function createKeychainOptions(
|
||||
capabilities,
|
||||
);
|
||||
|
||||
const setOptions: SetOptions = {
|
||||
const useStrongBox =
|
||||
options.useStrongBox ?? useSettingStore.getState().useStrongBox;
|
||||
|
||||
const setOptions: ExtendedSetOptions = {
|
||||
accessible: config.accessible,
|
||||
...(config.securityLevel && { securityLevel: config.securityLevel }),
|
||||
...(config.accessControl && { accessControl: config.accessControl }),
|
||||
useStrongBox,
|
||||
};
|
||||
|
||||
const getOptions: GetOptions = {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import type {
|
||||
@@ -15,6 +14,8 @@ import type {
|
||||
import { ExpandableBottomLayout as BaseExpandableBottomLayout } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { black } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import { SystemBars } from '@/components/SystemBars';
|
||||
|
||||
const Layout: React.FC<LayoutProps> = ({
|
||||
children,
|
||||
backgroundColor,
|
||||
|
||||
@@ -35,20 +35,26 @@ export default function SimpleScrolledTitleLayout({
|
||||
footer,
|
||||
}: DetailListProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const dismissBottomPadding = Math.min(16, insets.bottom);
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={white}>
|
||||
<ExpandableBottomLayout.FullSection paddingTop={0} flex={1}>
|
||||
<YStack paddingTop={insets.top + 12}>
|
||||
<YStack paddingTop={insets.top + 24}>
|
||||
<Title>{title}</Title>
|
||||
{header}
|
||||
</YStack>
|
||||
<ScrollView flex={1} showsVerticalScrollIndicator={false}>
|
||||
<ScrollView
|
||||
flex={1}
|
||||
showsVerticalScrollIndicator={true}
|
||||
indicatorStyle="black"
|
||||
scrollIndicatorInsets={{ right: 1 }}
|
||||
>
|
||||
<YStack paddingTop={0} paddingBottom={12} flex={1}>
|
||||
{children}
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
{footer && (
|
||||
<YStack marginTop={8} marginBottom={12}>
|
||||
<YStack marginTop={16} marginBottom={12}>
|
||||
{footer}
|
||||
</YStack>
|
||||
)}
|
||||
@@ -60,8 +66,8 @@ export default function SimpleScrolledTitleLayout({
|
||||
{secondaryButtonText}
|
||||
</SecondaryButton>
|
||||
)}
|
||||
{/* Anchor the Dismiss button to bottom with only safe area padding */}
|
||||
<YStack paddingBottom={insets.bottom + 8}>
|
||||
{/* Anchor the Dismiss button to bottom with sane spacing */}
|
||||
<YStack marginTop="auto" paddingBottom={dismissBottomPadding}>
|
||||
<PrimaryButton onPress={onDismiss}>Dismiss</PrimaryButton>
|
||||
</YStack>
|
||||
</ExpandableBottomLayout.FullSection>
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import type { NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
|
||||
import type { DocumentCategory } from '@selfxyz/common/utils/types';
|
||||
|
||||
import { SystemBars } from '@/components/SystemBars';
|
||||
import DeferredLinkingInfoScreen from '@/screens/app/DeferredLinkingInfoScreen';
|
||||
import GratificationScreen from '@/screens/app/GratificationScreen';
|
||||
import LoadingScreen from '@/screens/app/LoadingScreen';
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
import type { AlertModalParams } from '@/components/AlertModal';
|
||||
import AlertModal from '@/components/AlertModal';
|
||||
import FeedbackModal from '@/components/FeedbackModal';
|
||||
import type { FeedbackModalScreenParams } from '@/components/FeedbackModalScreen';
|
||||
import FeedbackModalScreen from '@/components/FeedbackModalScreen';
|
||||
import type { FeedbackType } from '@/hooks/useFeedbackModal';
|
||||
import { useFeedbackModal } from '@/hooks/useFeedbackModal';
|
||||
|
||||
@@ -19,7 +19,7 @@ interface FeedbackContextType {
|
||||
name?: string,
|
||||
email?: string,
|
||||
) => Promise<void>;
|
||||
showModal: (params: FeedbackModalScreenParams) => void;
|
||||
showModal: (params: AlertModalParams) => void;
|
||||
}
|
||||
|
||||
const FeedbackContext = createContext<FeedbackContextType | undefined>(
|
||||
@@ -50,13 +50,9 @@ export const FeedbackProvider: React.FC<FeedbackProviderProps> = ({
|
||||
>
|
||||
{children}
|
||||
|
||||
<FeedbackModal
|
||||
visible={isVisible}
|
||||
onClose={hideFeedbackModal}
|
||||
onSubmit={submitFeedback}
|
||||
/>
|
||||
<FeedbackModal visible={isVisible} onClose={hideFeedbackModal} />
|
||||
|
||||
<FeedbackModalScreen
|
||||
<AlertModal
|
||||
visible={isModalVisible}
|
||||
modalParams={modalParams}
|
||||
onHideModal={hideModal}
|
||||
|
||||
@@ -202,6 +202,10 @@ export function getAlternativeCSCA(
|
||||
useProtocolStore: SelfClient['useProtocolStore'],
|
||||
docCategory: DocumentCategory,
|
||||
): AlternativeCSCA {
|
||||
if (docCategory === 'kyc') {
|
||||
//TODO
|
||||
throw new Error('KYC is not supported yet');
|
||||
}
|
||||
if (docCategory === 'aadhaar') {
|
||||
const publicKeys = useProtocolStore.getState().aadhaar.public_keys;
|
||||
// Convert string[] to Record<string, string> format expected by AlternativeCSCA
|
||||
|
||||
@@ -111,6 +111,10 @@ const AccountRecoveryChoiceScreen: React.FC = () => {
|
||||
return useProtocolStore.getState()[docCategory].commitment_tree;
|
||||
},
|
||||
getAltCSCA(docCategory) {
|
||||
if (docCategory === 'kyc') {
|
||||
//TODO
|
||||
throw new Error('KYC is not supported yet');
|
||||
}
|
||||
if (docCategory === 'aadhaar') {
|
||||
const publicKeys =
|
||||
useProtocolStore.getState().aadhaar.public_keys;
|
||||
|
||||
@@ -99,6 +99,10 @@ const RecoverWithPhraseScreen: React.FC = () => {
|
||||
return useProtocolStore.getState()[docCategory].commitment_tree;
|
||||
},
|
||||
getAltCSCA(docCategory) {
|
||||
if (docCategory === 'kyc') {
|
||||
//TODO
|
||||
throw new Error('KYC is not supported yet');
|
||||
}
|
||||
if (docCategory === 'aadhaar') {
|
||||
const publicKeys =
|
||||
useProtocolStore.getState().aadhaar.public_keys;
|
||||
|
||||
@@ -18,12 +18,7 @@ import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
const ProofSettingsScreen: React.FC = () => {
|
||||
const {
|
||||
skipDocumentSelector,
|
||||
setSkipDocumentSelector,
|
||||
skipDocumentSelectorIfSingle,
|
||||
setSkipDocumentSelectorIfSingle,
|
||||
} = useSettingStore();
|
||||
const { skipDocumentSelector, setSkipDocumentSelector } = useSettingStore();
|
||||
|
||||
return (
|
||||
<YStack flex={1} backgroundColor={white}>
|
||||
@@ -49,35 +44,6 @@ const ProofSettingsScreen: React.FC = () => {
|
||||
testID="skip-document-selector-toggle"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
<View style={styles.settingRow}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={styles.settingLabel}>
|
||||
Skip when only one document
|
||||
</Text>
|
||||
<Text style={styles.settingDescription}>
|
||||
Automatically select your document when you only have one valid
|
||||
ID available
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={skipDocumentSelectorIfSingle}
|
||||
onValueChange={setSkipDocumentSelectorIfSingle}
|
||||
trackColor={{ false: slate200, true: blue600 }}
|
||||
thumbColor={white}
|
||||
disabled={skipDocumentSelector}
|
||||
testID="skip-document-selector-if-single-toggle"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{skipDocumentSelector && (
|
||||
<Text style={styles.infoText}>
|
||||
Document selection is always skipped. The "Skip when only one
|
||||
document" setting has no effect.
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
</YStack>
|
||||
@@ -114,17 +80,6 @@ const styles = StyleSheet.create({
|
||||
fontFamily: dinot,
|
||||
color: slate500,
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: slate200,
|
||||
},
|
||||
infoText: {
|
||||
fontSize: 13,
|
||||
fontFamily: dinot,
|
||||
fontStyle: 'italic',
|
||||
color: slate500,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export { ProofSettingsScreen };
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { PropsWithChildren } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Linking, Platform, Share, View as RNView } from 'react-native';
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||
import { getCountry, getLocales, getTimeZone } from 'react-native-localize';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import type { SvgProps } from 'react-native-svg';
|
||||
import { Button, ScrollView, View, XStack, YStack } from 'tamagui';
|
||||
@@ -45,10 +44,10 @@ import {
|
||||
} from '@/consts/links';
|
||||
import { impactLight } from '@/integrations/haptics';
|
||||
import { usePassport } from '@/providers/passportDataProvider';
|
||||
import { openSupportForm } from '@/services/support';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import { extraYPadding } from '@/utils/styleUtils';
|
||||
|
||||
import { version } from '../../../../package.json';
|
||||
// Avoid importing RootStackParamList to prevent type cycles; use minimal typing
|
||||
type MinimalRootStackParamList = Record<string, object | undefined>;
|
||||
|
||||
@@ -61,9 +60,8 @@ interface SocialButtonProps {
|
||||
href: string;
|
||||
}
|
||||
|
||||
const emailFeedback = 'support@self.xyz';
|
||||
// Avoid importing RootStackParamList; we only need string route names plus a few literals
|
||||
type RouteOption = string | 'share' | 'email_feedback' | 'ManageDocuments';
|
||||
type RouteOption = string | 'share' | 'support_form' | 'ManageDocuments';
|
||||
|
||||
const storeURL = Platform.OS === 'ios' ? appStoreUrl : playStoreUrl;
|
||||
|
||||
@@ -79,7 +77,7 @@ const routes =
|
||||
[Lock, 'Reveal recovery phrase', 'ShowRecoveryPhrase'],
|
||||
[Cloud, 'Cloud backup', 'CloudBackupSettings'],
|
||||
[Settings2 as React.FC<SvgProps>, 'Proof settings', 'ProofSettings'],
|
||||
[Feedback, 'Send feedback', 'email_feedback'],
|
||||
[Feedback, 'Get support', 'support_form'],
|
||||
[ShareIcon, 'Share Self app', 'share'],
|
||||
[
|
||||
FileText as React.FC<SvgProps>,
|
||||
@@ -90,7 +88,7 @@ const routes =
|
||||
: ([
|
||||
[Data, 'View document info', 'DocumentDataInfo'],
|
||||
[Settings2 as React.FC<SvgProps>, 'Proof settings', 'ProofSettings'],
|
||||
[Feedback, 'Send feeback', 'email_feedback'],
|
||||
[Feedback, 'Get support', 'support_form'],
|
||||
[
|
||||
FileText as React.FC<SvgProps>,
|
||||
'Manage ID documents',
|
||||
@@ -222,32 +220,17 @@ const SettingsScreen: React.FC = () => {
|
||||
);
|
||||
break;
|
||||
|
||||
case 'email_feedback':
|
||||
const subject = 'SELF App Feedback';
|
||||
const deviceInfo = [
|
||||
['device', `${Platform.OS}@${Platform.Version}`],
|
||||
['app', `v${version}`],
|
||||
[
|
||||
'locales',
|
||||
getLocales()
|
||||
.map(locale => `${locale.languageCode}-${locale.countryCode}`)
|
||||
.join(','),
|
||||
],
|
||||
['country', getCountry()],
|
||||
['tz', getTimeZone()],
|
||||
['ts', new Date()],
|
||||
['origin', 'settings/feedback'],
|
||||
] as [string, string][];
|
||||
|
||||
const body = `
|
||||
---
|
||||
${deviceInfo.map(([k, v]) => `${k}=${v}`).join('; ')}
|
||||
---`;
|
||||
await Linking.openURL(
|
||||
`mailto:${emailFeedback}?subject=${encodeURIComponent(
|
||||
subject,
|
||||
)}&body=${encodeURIComponent(body)}`,
|
||||
);
|
||||
case 'support_form':
|
||||
try {
|
||||
await openSupportForm();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'SettingsScreen: failed to open support form:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
// Error is already handled and displayed to user in openSupportForm,
|
||||
// but we log here for debugging purposes
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ManageDocuments':
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
StyleSheet,
|
||||
Text as RNText,
|
||||
} from 'react-native';
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Text, View, YStack } from 'tamagui';
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
@@ -28,6 +27,7 @@ import { dinot, dinotBold } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import GratificationBg from '@/assets/images/gratification_bg.svg';
|
||||
import SelfLogo from '@/assets/logos/self.svg';
|
||||
import { SystemBars } from '@/components/SystemBars';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
|
||||
const GratificationScreen: React.FC = () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
||||
import { Alert, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { Alert, Platform, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { Button, Sheet, Text, XStack, YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
@@ -399,6 +399,8 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
const subscribedTopics = useSettingStore(state => state.subscribedTopics);
|
||||
const loggingSeverity = useSettingStore(state => state.loggingSeverity);
|
||||
const setLoggingSeverity = useSettingStore(state => state.setLoggingSeverity);
|
||||
const useStrongBox = useSettingStore(state => state.useStrongBox);
|
||||
const setUseStrongBox = useSettingStore(state => state.setUseStrongBox);
|
||||
const [hasNotificationPermission, setHasNotificationPermission] =
|
||||
useState(false);
|
||||
const paddingBottom = useSafeBottomPadding(20);
|
||||
@@ -754,6 +756,34 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
/>
|
||||
</ParameterSection>
|
||||
|
||||
{Platform.OS === 'android' && (
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Android Keystore"
|
||||
description="Configure keystore security options"
|
||||
>
|
||||
<TopicToggleButton
|
||||
label="Use StrongBox"
|
||||
isSubscribed={useStrongBox}
|
||||
onToggle={() => {
|
||||
Alert.alert(
|
||||
useStrongBox ? 'Disable StrongBox' : 'Enable StrongBox',
|
||||
useStrongBox
|
||||
? 'New keys will be generated without StrongBox hardware backing. Existing keys will continue to work.'
|
||||
: 'New keys will attempt to use StrongBox hardware backing for enhanced security.',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: useStrongBox ? 'Disable' : 'Enable',
|
||||
onPress: () => setUseStrongBox(!useStrongBox),
|
||||
},
|
||||
],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ParameterSection>
|
||||
)}
|
||||
|
||||
<ParameterSection
|
||||
icon={<WarningIcon color={yellow500} />}
|
||||
title="Danger Zone"
|
||||
|
||||
@@ -109,7 +109,8 @@ const PassportDataSelector = () => {
|
||||
try {
|
||||
await setSelectedDocument(documentId);
|
||||
navigation.navigate('ConfirmBelonging', {});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error('Failed to navigate to registration:', error);
|
||||
Alert.alert(
|
||||
'Registration Error',
|
||||
'Failed to prepare document for registration. Please try again.',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Platform, ScrollView } from 'react-native';
|
||||
import { Input, YStack } from 'tamagui';
|
||||
import { Input, Switch, XStack, YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
@@ -21,6 +21,7 @@ import { white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
type NFCParams = {
|
||||
skipPACE?: boolean;
|
||||
@@ -111,6 +112,10 @@ const DocumentNFCMethodSelectionScreen: React.FC = () => {
|
||||
const { useMRZStore } = selfClient;
|
||||
const { update, passportNumber, dateOfBirth, dateOfExpiry } = useMRZStore();
|
||||
|
||||
const loggingSeverity = useSettingStore(state => state.loggingSeverity);
|
||||
const setLoggingSeverity = useSettingStore(state => state.setLoggingSeverity);
|
||||
const isDebugMode = loggingSeverity === 'debug';
|
||||
|
||||
const handleSelect = (key: string) => {
|
||||
setSelectedMethod(key);
|
||||
setError('');
|
||||
@@ -153,6 +158,30 @@ const DocumentNFCMethodSelectionScreen: React.FC = () => {
|
||||
<YStack paddingTop={20} gap={20}>
|
||||
<Title>Choose NFC Scan Method</Title>
|
||||
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingHorizontal="$2"
|
||||
borderWidth={1}
|
||||
borderColor="#ccc"
|
||||
borderRadius={10}
|
||||
backgroundColor="#fff"
|
||||
>
|
||||
<Description>Debug Logging</Description>
|
||||
<Switch
|
||||
size="$4"
|
||||
checked={isDebugMode}
|
||||
onCheckedChange={checked => {
|
||||
setLoggingSeverity(checked ? 'debug' : 'warn');
|
||||
}}
|
||||
backgroundColor={isDebugMode ? '$green7Light' : '$gray4'}
|
||||
style={{ minWidth: 48, minHeight: 36 }}
|
||||
>
|
||||
<Switch.Thumb animation="quick" backgroundColor="$white" />
|
||||
</Switch>
|
||||
</XStack>
|
||||
|
||||
{NFC_METHODS.filter(method =>
|
||||
method.platform.includes(Platform.OS),
|
||||
).map(method => (
|
||||
|
||||
@@ -73,7 +73,11 @@ import {
|
||||
setNfcScanningActive,
|
||||
trackNfcEvent,
|
||||
} from '@/services/analytics';
|
||||
import { sendFeedbackEmail } from '@/services/email';
|
||||
import {
|
||||
openSupportForm,
|
||||
SUPPORT_FORM_BUTTON_TEXT,
|
||||
SUPPORT_FORM_MESSAGE,
|
||||
} from '@/services/support';
|
||||
|
||||
const emitter =
|
||||
Platform.OS === 'android'
|
||||
@@ -170,10 +174,7 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
});
|
||||
|
||||
const onReportIssue = useCallback(() => {
|
||||
sendFeedbackEmail({
|
||||
message: 'User reported an issue from NFC scan screen',
|
||||
origin: 'passport/nfc',
|
||||
});
|
||||
openSupportForm();
|
||||
}, []);
|
||||
|
||||
const openErrorModal = useCallback(
|
||||
@@ -191,14 +192,10 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
showModal({
|
||||
titleText: 'NFC Scan Error',
|
||||
bodyText: message,
|
||||
buttonText: 'Report Issue',
|
||||
buttonText: SUPPORT_FORM_BUTTON_TEXT,
|
||||
secondaryButtonText: 'Help',
|
||||
preventDismiss: false,
|
||||
onButtonPress: () =>
|
||||
sendFeedbackEmail({
|
||||
message: sanitizeErrorMessage(message),
|
||||
origin: 'passport/nfc',
|
||||
}),
|
||||
onButtonPress: openSupportForm,
|
||||
onSecondaryButtonPress: goToNFCTrouble,
|
||||
onModalDismiss: () => {},
|
||||
});
|
||||
@@ -426,7 +423,7 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
});
|
||||
openErrorModal(message);
|
||||
// We deliberately avoid opening any external feedback widgets here;
|
||||
// users can send feedback via the email action in the modal.
|
||||
// users can request support via the support form action in the modal.
|
||||
} finally {
|
||||
if (scanTimeoutRef.current) {
|
||||
clearTimeout(scanTimeoutRef.current);
|
||||
@@ -612,6 +609,9 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
</BodyText>
|
||||
</>
|
||||
)}
|
||||
<BodyText style={[styles.disclaimer, { marginTop: 12 }]}>
|
||||
{SUPPORT_FORM_MESSAGE}
|
||||
</BodyText>
|
||||
</TextsContainer>
|
||||
<ButtonsContainer>
|
||||
<PrimaryButton
|
||||
@@ -634,7 +634,7 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<SecondaryButton onPress={onReportIssue}>
|
||||
Report Issue
|
||||
{SUPPORT_FORM_BUTTON_TEXT}
|
||||
</SecondaryButton>
|
||||
</ButtonsContainer>
|
||||
</>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useFeedbackAutoHide } from '@/hooks/useFeedbackAutoHide';
|
||||
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
||||
import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout';
|
||||
import { flushAllAnalytics } from '@/services/analytics';
|
||||
import { sendFeedbackEmail } from '@/services/email';
|
||||
import { openSupportForm, SUPPORT_FORM_BUTTON_TEXT } from '@/services/support';
|
||||
|
||||
const tips: TipProps[] = [
|
||||
{
|
||||
@@ -71,20 +71,9 @@ const DocumentNFCTroubleScreen: React.FC = () => {
|
||||
secondaryButtonText="Open NFC Options"
|
||||
onSecondaryButtonPress={goToNFCMethodSelection}
|
||||
footer={
|
||||
// Add top padding before buttons and normalize spacing
|
||||
<YStack marginTop={16} marginBottom={0} gap={10}>
|
||||
<SecondaryButton
|
||||
onPress={() =>
|
||||
sendFeedbackEmail({
|
||||
message: 'User reported an issue from NFC trouble screen',
|
||||
origin: 'passport/nfc-trouble',
|
||||
})
|
||||
}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
Report Issue
|
||||
</SecondaryButton>
|
||||
</YStack>
|
||||
<SecondaryButton onPress={openSupportForm} style={{ marginBottom: 0 }}>
|
||||
{SUPPORT_FORM_BUTTON_TEXT}
|
||||
</SecondaryButton>
|
||||
}
|
||||
>
|
||||
<YStack
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import LottieView from 'lottie-react-native';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
@@ -58,7 +57,6 @@ const DocumentOnboardingScreen: React.FC = () => {
|
||||
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={black}>
|
||||
<SystemBars style="light" />
|
||||
<ExpandableBottomLayout.TopSection roundTop backgroundColor={black}>
|
||||
<LottieView
|
||||
ref={animationRef}
|
||||
|
||||
@@ -26,7 +26,11 @@ import { notificationError } from '@/integrations/haptics';
|
||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import type { SharedRoutesParamList } from '@/navigation/types';
|
||||
import { flush as flushAnalytics } from '@/services/analytics';
|
||||
import { sendCountrySupportNotification } from '@/services/email';
|
||||
import {
|
||||
openSupportForm,
|
||||
SUPPORT_FORM_COMING_SOON_BUTTON_TEXT,
|
||||
SUPPORT_FORM_COMING_SOON_MESSAGE,
|
||||
} from '@/services/support';
|
||||
|
||||
type ComingSoonScreenProps = NativeStackScreenProps<
|
||||
SharedRoutesParamList,
|
||||
@@ -83,13 +87,9 @@ const ComingSoonScreen: React.FC<ComingSoonScreenProps> = ({ route }) => {
|
||||
|
||||
const onNotifyMe = async () => {
|
||||
try {
|
||||
await sendCountrySupportNotification({
|
||||
countryName,
|
||||
countryCode: countryCode !== 'Unknown' ? countryCode : '',
|
||||
documentCategory: route.params?.documentCategory,
|
||||
});
|
||||
await openSupportForm();
|
||||
} catch (error) {
|
||||
console.error('Failed to open email client:', error);
|
||||
console.error('Failed to open support form:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -101,13 +101,11 @@ const ComingSoonScreen: React.FC<ComingSoonScreenProps> = ({ route }) => {
|
||||
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={black}>
|
||||
<ExpandableBottomLayout.TopSection backgroundColor={white}>
|
||||
<YStack
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
marginTop={100}
|
||||
>
|
||||
<ExpandableBottomLayout.TopSection
|
||||
backgroundColor={white}
|
||||
overflow="visible"
|
||||
>
|
||||
<YStack flex={1} justifyContent="center" alignItems="center">
|
||||
<XStack
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
@@ -124,6 +122,7 @@ const ComingSoonScreen: React.FC<ComingSoonScreenProps> = ({ route }) => {
|
||||
textAlign: 'center',
|
||||
color: black,
|
||||
marginBottom: 16,
|
||||
paddingTop: 10,
|
||||
}}
|
||||
>
|
||||
Coming Soon
|
||||
@@ -150,7 +149,7 @@ const ComingSoonScreen: React.FC<ComingSoonScreenProps> = ({ route }) => {
|
||||
paddingHorizontal: 10,
|
||||
}}
|
||||
>
|
||||
Sign up for live updates.
|
||||
{SUPPORT_FORM_COMING_SOON_MESSAGE}
|
||||
</BodyText>
|
||||
</YStack>
|
||||
</ExpandableBottomLayout.TopSection>
|
||||
@@ -158,13 +157,14 @@ const ComingSoonScreen: React.FC<ComingSoonScreenProps> = ({ route }) => {
|
||||
gap={16}
|
||||
backgroundColor={white}
|
||||
paddingHorizontal={20}
|
||||
paddingVertical={20}
|
||||
paddingTop={20}
|
||||
paddingBottom={20}
|
||||
>
|
||||
<PrimaryButton
|
||||
onPress={onNotifyMe}
|
||||
trackEvent={PassportEvents.NOTIFY_COMING_SOON}
|
||||
>
|
||||
Sign up for updates
|
||||
{SUPPORT_FORM_COMING_SOON_BUTTON_TEXT}
|
||||
</PrimaryButton>
|
||||
<SecondaryButton
|
||||
trackEvent={PassportEvents.DISMISS_COMING_SOON}
|
||||
|
||||
@@ -71,7 +71,7 @@ const StarfallPushCodeScreen: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleCopyCode = async () => {
|
||||
if (!code || code === DASH_CODE) {
|
||||
if (isLoading || isCopied || !code || code === DASH_CODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -225,14 +225,18 @@ const StarfallPushCodeScreen: React.FC = () => {
|
||||
|
||||
<PrimaryButton
|
||||
onPress={handleCopyCode}
|
||||
disabled={isCopied || !code || code === DASH_CODE || isLoading}
|
||||
fontSize={16}
|
||||
accessibilityState={{
|
||||
disabled: isCopied || !code || code === DASH_CODE || isLoading,
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: isCopied ? green500 : undefined,
|
||||
borderColor: '#374151',
|
||||
borderWidth: 1,
|
||||
borderRadius: 60,
|
||||
height: 46,
|
||||
opacity:
|
||||
isCopied || !code || code === DASH_CODE || isLoading ? 0.6 : 1,
|
||||
paddingVertical: 0,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -219,6 +219,7 @@ const DocumentSelectorForProvingScreen: React.FC = () => {
|
||||
|
||||
const documents = useMemo(() => {
|
||||
return documentCatalog.documents
|
||||
.filter(metadata => metadata.isRegistered)
|
||||
.map(metadata => {
|
||||
const docData = allDocuments[metadata.id];
|
||||
const baseState = determineDocumentState(metadata, docData?.data);
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { LottieViewProps } from 'lottie-react-native';
|
||||
import LottieView from 'lottie-react-native';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Linking, StyleSheet, View } from 'react-native';
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import { ScrollView, Spinner } from 'tamagui';
|
||||
import { useIsFocused, useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
@@ -210,7 +209,6 @@ const SuccessScreen: React.FC = () => {
|
||||
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={white}>
|
||||
<SystemBars style="dark" />
|
||||
<ExpandableBottomLayout.TopSection
|
||||
roundTop
|
||||
marginTop={20}
|
||||
|
||||
@@ -26,7 +26,7 @@ import { getDocumentTypeName } from '@/utils/documentUtils';
|
||||
*
|
||||
* This screen:
|
||||
* 1. Loads document catalog and counts valid documents
|
||||
* 2. Checks skip settings (skipDocumentSelector, skipDocumentSelectorIfSingle)
|
||||
* 2. Checks skip settings (skipDocumentSelector, auto-skip on single document)
|
||||
* 3. Routes to appropriate screen:
|
||||
* - No valid documents -> DocumentDataNotFound
|
||||
* - Skip enabled -> auto-select and go to Prove
|
||||
@@ -37,8 +37,7 @@ const ProvingScreenRouter: React.FC = () => {
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const { loadDocumentCatalog, getAllDocuments, setSelectedDocument } =
|
||||
usePassport();
|
||||
const { skipDocumentSelector, skipDocumentSelectorIfSingle } =
|
||||
useSettingStore();
|
||||
const { skipDocumentSelector } = useSettingStore();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const hasRoutedRef = useRef(false);
|
||||
@@ -87,9 +86,7 @@ const ProvingScreenRouter: React.FC = () => {
|
||||
const documentType = getDocumentTypeName(firstValidDoc?.documentCategory);
|
||||
|
||||
// Determine if we should skip the selector
|
||||
const shouldSkip =
|
||||
skipDocumentSelector ||
|
||||
(skipDocumentSelectorIfSingle && validCount === 1);
|
||||
const shouldSkip = skipDocumentSelector || validCount === 1;
|
||||
|
||||
if (shouldSkip) {
|
||||
// Auto-select and navigate to Prove
|
||||
@@ -134,7 +131,6 @@ const ProvingScreenRouter: React.FC = () => {
|
||||
navigation,
|
||||
setSelectedDocument,
|
||||
skipDocumentSelector,
|
||||
skipDocumentSelectorIfSingle,
|
||||
]);
|
||||
|
||||
useFocusEffect(
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { View } from 'tamagui';
|
||||
|
||||
import { Caption } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import { Caption, SecondaryButton } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import { slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import type { TipProps } from '@/components/Tips';
|
||||
@@ -12,6 +13,11 @@ import Tips from '@/components/Tips';
|
||||
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
||||
import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout';
|
||||
import { flushAllAnalytics } from '@/services/analytics';
|
||||
import {
|
||||
openSupportForm,
|
||||
SUPPORT_FORM_BUTTON_TEXT,
|
||||
SUPPORT_FORM_TIP_MESSAGE,
|
||||
} from '@/services/support';
|
||||
|
||||
const tips: TipProps[] = [
|
||||
{
|
||||
@@ -39,7 +45,7 @@ const tips: TipProps[] = [
|
||||
const tipsDeeplink: TipProps[] = [
|
||||
{
|
||||
title: 'Coming from another app/website?',
|
||||
body: 'Please contact the support, a telegram group is available in the options menu.',
|
||||
body: SUPPORT_FORM_TIP_MESSAGE,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -55,12 +61,19 @@ const QRCodeTrouble: React.FC = () => {
|
||||
<SimpleScrolledTitleLayout
|
||||
title="Having trouble scanning the QR code?"
|
||||
onDismiss={go}
|
||||
footer={
|
||||
<SecondaryButton onPress={openSupportForm}>
|
||||
{SUPPORT_FORM_BUTTON_TEXT}
|
||||
</SecondaryButton>
|
||||
}
|
||||
>
|
||||
<Caption size="large" style={{ color: slate500 }}>
|
||||
<Caption size="large" style={{ color: slate500, marginBottom: 16 }}>
|
||||
Here are some tips to help you successfully scan the QR code:
|
||||
</Caption>
|
||||
<Tips items={tips} />
|
||||
<Tips items={tipsDeeplink} />
|
||||
<View marginBottom={24}>
|
||||
<Tips items={tips} />
|
||||
<Tips items={tipsDeeplink} />
|
||||
</View>
|
||||
</SimpleScrolledTitleLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { Linking, Platform } from 'react-native';
|
||||
import { getCountry, getLocales, getTimeZone } from 'react-native-localize';
|
||||
|
||||
import { sanitizeErrorMessage } from '@selfxyz/mobile-sdk-alpha/utils/utils';
|
||||
|
||||
import { version } from '../../package.json';
|
||||
|
||||
interface SendFeedbackEmailOptions {
|
||||
message: string;
|
||||
origin: string;
|
||||
subject?: string;
|
||||
recipient?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification email requesting support for a specific country
|
||||
* @param options Configuration for the country support notification email
|
||||
*/
|
||||
export const sendCountrySupportNotification = async ({
|
||||
countryName,
|
||||
countryCode,
|
||||
documentCategory,
|
||||
subject = `Country Support Request: ${countryName}`,
|
||||
recipient = 'support@self.xyz',
|
||||
}: SendCountrySupportNotificationOptions): Promise<void> => {
|
||||
const deviceInfo = [
|
||||
['device', `${Platform.OS}@${Platform.Version}`],
|
||||
['app', `v${version}`],
|
||||
[
|
||||
'locales',
|
||||
getLocales()
|
||||
.map(locale => `${locale.languageCode}-${locale.countryCode}`)
|
||||
.join(','),
|
||||
],
|
||||
['userCountry', getCountry()],
|
||||
['requestedCountry', countryCode || 'Unknown'],
|
||||
['documentCategory', documentCategory || 'Unknown'],
|
||||
['tz', getTimeZone()],
|
||||
['ts', new Date().toISOString()],
|
||||
['origin', 'coming_soon_screen'],
|
||||
] as [string, string][];
|
||||
|
||||
const documentTypeText =
|
||||
documentCategory === 'id_card'
|
||||
? 'ID cards'
|
||||
: documentCategory === 'passport'
|
||||
? 'passports'
|
||||
: 'documents';
|
||||
|
||||
const body = `Hi SELF Team,
|
||||
|
||||
I would like to request support for ${countryName} ${documentTypeText} in the SELF app. Please notify me when support becomes available.
|
||||
|
||||
Additional comments (optional):
|
||||
|
||||
|
||||
---
|
||||
Technical Details (do not modify):
|
||||
${deviceInfo.map(([k, v]) => `${k}=${v}`).join('\n')}
|
||||
---`;
|
||||
|
||||
await Linking.openURL(
|
||||
`mailto:${recipient}?subject=${encodeURIComponent(
|
||||
subject,
|
||||
)}&body=${encodeURIComponent(body)}`,
|
||||
);
|
||||
};
|
||||
|
||||
interface SendCountrySupportNotificationOptions {
|
||||
countryName: string;
|
||||
countryCode?: string;
|
||||
documentCategory?: string;
|
||||
subject?: string;
|
||||
recipient?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a feedback email with device information and user message
|
||||
* @param options Configuration for the feedback email
|
||||
*/
|
||||
export const sendFeedbackEmail = async ({
|
||||
message,
|
||||
origin,
|
||||
subject = 'SELF App Feedback',
|
||||
recipient = 'support@self.xyz',
|
||||
}: SendFeedbackEmailOptions): Promise<void> => {
|
||||
const deviceInfo = [
|
||||
['device', `${Platform.OS}@${Platform.Version}`],
|
||||
['app', `v${version}`],
|
||||
[
|
||||
'locales',
|
||||
getLocales()
|
||||
.map(locale => `${locale.languageCode}-${locale.countryCode}`)
|
||||
.join(','),
|
||||
],
|
||||
['country', getCountry()],
|
||||
['tz', getTimeZone()],
|
||||
['ts', new Date().toISOString()],
|
||||
['origin', origin],
|
||||
['error', sanitizeErrorMessage(message)],
|
||||
] as [string, string][];
|
||||
|
||||
const body = `Please describe the issue you're experiencing:
|
||||
|
||||
---
|
||||
Technical Details (do not modify):
|
||||
${deviceInfo.map(([k, v]) => `${k}=${v}`).join('\n')}
|
||||
---`;
|
||||
|
||||
await Linking.openURL(
|
||||
`mailto:${recipient}?subject=${encodeURIComponent(
|
||||
subject,
|
||||
)}&body=${encodeURIComponent(body)}`,
|
||||
);
|
||||
};
|
||||
42
app/src/services/support.ts
Normal file
42
app/src/services/support.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { Alert, Linking } from 'react-native';
|
||||
|
||||
import { supportFormUrl } from '@/consts/links';
|
||||
|
||||
export const SUPPORT_FORM_BUTTON_TEXT = 'Send feedback';
|
||||
|
||||
export const SUPPORT_FORM_COMING_SOON_BUTTON_TEXT = 'Let us know';
|
||||
|
||||
export const SUPPORT_FORM_COMING_SOON_MESSAGE =
|
||||
'Want your document supported? Let us know.';
|
||||
|
||||
export const SUPPORT_FORM_MESSAGE = 'Have feedback? Please fill out our form.';
|
||||
|
||||
export const SUPPORT_FORM_TIP_MESSAGE = 'Have feedback? Let us know.';
|
||||
|
||||
export const openSupportForm = async (): Promise<void> => {
|
||||
try {
|
||||
const canOpen = await Linking.canOpenURL(supportFormUrl);
|
||||
if (canOpen) {
|
||||
await Linking.openURL(supportFormUrl);
|
||||
} else {
|
||||
console.warn('Cannot open support form URL - no handler available');
|
||||
Alert.alert(
|
||||
'Unable to Open Link',
|
||||
'No app is available to open the support form. Please try again using a web browser.',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to open support form:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'Unable to open support form. Please try again later or contact support through another method.',
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -35,14 +35,14 @@ interface PersistedSettingsState {
|
||||
setLoggingSeverity: (severity: LoggingSeverity) => void;
|
||||
setPointsAddress: (address: string | null) => void;
|
||||
setSkipDocumentSelector: (value: boolean) => void;
|
||||
setSkipDocumentSelectorIfSingle: (value: boolean) => void;
|
||||
setSubscribedTopics: (topics: string[]) => void;
|
||||
setTurnkeyBackupEnabled: (turnkeyBackupEnabled: boolean) => void;
|
||||
setUseStrongBox: (useStrongBox: boolean) => void;
|
||||
skipDocumentSelector: boolean;
|
||||
skipDocumentSelectorIfSingle: boolean;
|
||||
subscribedTopics: string[];
|
||||
toggleCloudBackupEnabled: () => void;
|
||||
turnkeyBackupEnabled: boolean;
|
||||
useStrongBox: boolean;
|
||||
}
|
||||
|
||||
interface NonPersistedSettingsState {
|
||||
@@ -143,9 +143,10 @@ export const useSettingStore = create<SettingsState>()(
|
||||
skipDocumentSelector: false,
|
||||
setSkipDocumentSelector: (value: boolean) =>
|
||||
set({ skipDocumentSelector: value }),
|
||||
skipDocumentSelectorIfSingle: true,
|
||||
setSkipDocumentSelectorIfSingle: (value: boolean) =>
|
||||
set({ skipDocumentSelectorIfSingle: value }),
|
||||
|
||||
// StrongBox setting for Android keystore (default: false)
|
||||
useStrongBox: false,
|
||||
setUseStrongBox: (useStrongBox: boolean) => set({ useStrongBox }),
|
||||
|
||||
// Non-persisted state (will not be saved to storage)
|
||||
hideNetworkModal: false,
|
||||
|
||||
21
app/src/types/react-native-keychain.d.ts
vendored
Normal file
21
app/src/types/react-native-keychain.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { SetOptions } from 'react-native-keychain';
|
||||
|
||||
/**
|
||||
* Extended SetOptions with useStrongBox property added by our patch.
|
||||
* Use this type when you need the useStrongBox option for Android keystore.
|
||||
*/
|
||||
export type ExtendedSetOptions = SetOptions & {
|
||||
/**
|
||||
* Whether to attempt StrongBox-backed key generation on Android.
|
||||
* When true (default), the library will try to use StrongBox hardware
|
||||
* security module if available, falling back to regular secure hardware.
|
||||
* When false, StrongBox is skipped and regular secure hardware is used directly.
|
||||
* @platform Android
|
||||
* @default true
|
||||
*/
|
||||
useStrongBox?: boolean;
|
||||
};
|
||||
@@ -29,6 +29,8 @@ export function isKeychainCryptoError(error: unknown): boolean {
|
||||
err?.name === 'com.oblador.keychain.exceptions.CryptoFailedException' ||
|
||||
err?.message?.includes('CryptoFailedException') ||
|
||||
err?.message?.includes('Decryption failed') ||
|
||||
err?.message?.includes('Could not encrypt data') ||
|
||||
err?.message?.includes('Keystore operation failed') ||
|
||||
err?.message?.includes('Authentication tag verification failed')) &&
|
||||
!isUserCancellation(error),
|
||||
);
|
||||
@@ -41,6 +43,13 @@ export function isUserCancellation(error: unknown): boolean {
|
||||
err?.code === 'USER_CANCELED' ||
|
||||
err?.message?.includes('User canceled') ||
|
||||
err?.message?.includes('Authentication canceled') ||
|
||||
err?.message?.includes('cancelled by user'),
|
||||
err?.message?.includes('cancelled by user') ||
|
||||
err?.message?.includes('Fingerprint operation cancelled') ||
|
||||
err?.message?.includes('operation cancelled') ||
|
||||
err?.message?.includes("Can't verify face") ||
|
||||
err?.message?.includes('code: 5') || // ERROR_CANCELED
|
||||
err?.message?.includes('code: 2') || // ERROR_UNABLE_TO_PROCESS (biometric verification failed)
|
||||
err?.message?.includes('code: 10') || // ERROR_USER_CANCELED
|
||||
err?.message?.includes('code: 13'), // ERROR_NEGATIVE_BUTTON - for ref (https://developer.android.com/reference/androidx/biometric/BiometricPrompt#ERROR_NEGATIVE_BUTTON())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ jest.mock('react-native', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
Dimensions: mockDimensions,
|
||||
Platform: { OS: 'ios', select: jest.fn() },
|
||||
Pressable: ({ onPress, children }: any) => (
|
||||
<button onClick={onPress} type="button">
|
||||
{children}
|
||||
|
||||
@@ -184,15 +184,23 @@ describe('ProvingScreenRouter', () => {
|
||||
});
|
||||
|
||||
it('routes to the document selector when skipping is disabled', async () => {
|
||||
const passport = createMetadata({
|
||||
const passport1 = createMetadata({
|
||||
id: 'doc-1',
|
||||
documentType: 'us',
|
||||
isRegistered: true,
|
||||
});
|
||||
const passport2 = createMetadata({
|
||||
id: 'doc-2',
|
||||
documentType: 'gb',
|
||||
isRegistered: true,
|
||||
});
|
||||
const catalog: DocumentCatalog = {
|
||||
documents: [passport],
|
||||
documents: [passport1, passport2],
|
||||
};
|
||||
const allDocs = createAllDocuments([createDocumentEntry(passport)]);
|
||||
const allDocs = createAllDocuments([
|
||||
createDocumentEntry(passport1),
|
||||
createDocumentEntry(passport2),
|
||||
]);
|
||||
|
||||
mockLoadDocumentCatalog.mockResolvedValue(catalog);
|
||||
mockGetAllDocuments.mockResolvedValue(allDocs);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"lastDeployed": "2026-01-12T23:27:08.229Z"
|
||||
},
|
||||
"android": {
|
||||
"build": 134,
|
||||
"lastDeployed": "2026-01-12T16:10:12.854Z"
|
||||
"build": 136,
|
||||
"lastDeployed": "2026-01-15T16:44:42Z"
|
||||
}
|
||||
}
|
||||
|
||||
137
circuits/circuits/disclose/vc_and_disclose_kyc.circom
Normal file
137
circuits/circuits/disclose/vc_and_disclose_kyc.circom
Normal file
@@ -0,0 +1,137 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "circomlib/circuits/bitify.circom";
|
||||
include "circomlib/circuits/poseidon.circom";
|
||||
include "@zk-kit/binary-merkle-root.circom/src/binary-merkle-root.circom";
|
||||
include "@openpassport/zk-email-circuits/utils/bytes.circom";
|
||||
include "../utils/passport/customHashers.circom";
|
||||
include "../utils/kyc/disclose/disclose.circom";
|
||||
|
||||
/// @title VC_AND_DISCLOSE_KYC
|
||||
/// @notice Combines verifiable credential verification with KYC disclosure, validating data against a merkle tree and performing comprehensive compliance checks
|
||||
/// @param MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH Maximum number of countries in the forbidden countries list
|
||||
/// @param namedobTreeLevels Depth of the sparse merkle tree for OFAC name+DOB verification
|
||||
/// @param nameyobTreeLevels Depth of the sparse merkle tree for OFAC name+YOB verification
|
||||
/// @param nLevels Depth of the binary merkle tree for credential verification
|
||||
/// @input data_padded The padded KYC data containing all document fields
|
||||
/// @input compressed_disclose_sel Two-element array representing compressed disclosure selector
|
||||
/// @input scope Application-specific scope identifier for nullifier generation
|
||||
/// @input forbidden_countries_list Flat array of forbidden country codes
|
||||
/// @input merkle_root Root of the binary merkle tree for credential verification
|
||||
/// @input leaf_depth Depth of the leaf in the merkle tree
|
||||
/// @input path Binary path to the leaf in the merkle tree
|
||||
/// @input siblings Sibling nodes for merkle proof verification
|
||||
/// @input ofac_name_dob_smt_leaf_key Leaf key for OFAC name+DOB sparse merkle tree verification
|
||||
/// @input ofac_name_dob_smt_root Root of the OFAC name+DOB sparse merkle tree
|
||||
/// @input ofac_name_dob_smt_siblings Sibling nodes for OFAC name+DOB merkle proof
|
||||
/// @input ofac_name_yob_smt_leaf_key Leaf key for OFAC name+YOB sparse merkle tree verification
|
||||
/// @input ofac_name_yob_smt_root Root of the OFAC name+YOB sparse merkle tree
|
||||
/// @input ofac_name_yob_smt_siblings Sibling nodes for OFAC name+YOB merkle proof
|
||||
/// @input selector_ofac Binary selector to enable/disable OFAC checks (0 or 1)
|
||||
/// @input user_identifier Unique identifier for the user
|
||||
/// @input current_date Current date in YYYYMMDD format
|
||||
/// @input majority_age_ASCII Age threshold for majority verification (ASCII encoded, 3 digits)
|
||||
/// @input secret Secret value used for leaf generation and nullifier computation
|
||||
/// @input attestation_id Unique identifier for this attestation
|
||||
/// @output revealedData_packed Packed array containing selectively revealed data fields and verification results
|
||||
/// @output forbidden_countries_list_packed Packed representation of the forbidden countries list
|
||||
/// @output nullifier Unique nullifier derived from secret and scope to prevent double-spending
|
||||
/// @dev Verifies the credential against a binary merkle tree
|
||||
/// @dev The compressed_disclose_sel is split into two 133-bit values and reconstructed into a selector array
|
||||
/// @dev TODO: we can pass majority_age(number) rather than majority_age_ASCII which will save few constraints, but kept it for now to match with other circuits
|
||||
|
||||
template VC_AND_DISCLOSE_KYC(
|
||||
MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH,
|
||||
namedobTreeLevels,
|
||||
nameyobTreeLevels,
|
||||
nLevels
|
||||
) {
|
||||
var max_length = KYC_MAX_LENGTH();
|
||||
var country_length = COUNTRY_LENGTH();
|
||||
var id_number_length = ID_NUMBER_LENGTH();
|
||||
var idNumberIdx = ID_NUMBER_INDEX();
|
||||
var compressed_bit_len = max_length/2;
|
||||
|
||||
signal input data_padded[max_length];
|
||||
signal input compressed_disclose_sel[2];
|
||||
|
||||
signal input scope;
|
||||
|
||||
signal input forbidden_countries_list[MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH * country_length];
|
||||
|
||||
signal input merkle_root;
|
||||
signal input leaf_depth;
|
||||
signal input path[nLevels];
|
||||
signal input siblings[nLevels];
|
||||
|
||||
signal input ofac_name_dob_smt_leaf_key;
|
||||
signal input ofac_name_dob_smt_root;
|
||||
signal input ofac_name_dob_smt_siblings[namedobTreeLevels];
|
||||
|
||||
signal input ofac_name_yob_smt_leaf_key;
|
||||
signal input ofac_name_yob_smt_root;
|
||||
signal input ofac_name_yob_smt_siblings[nameyobTreeLevels];
|
||||
|
||||
signal input selector_ofac;
|
||||
signal input user_identifier;
|
||||
signal input current_date[8];
|
||||
signal input majority_age_ASCII[3];
|
||||
signal input secret;
|
||||
|
||||
// Convert the two decimal inputs back to bit array
|
||||
signal disclose_sel[max_length];
|
||||
|
||||
// Convert disclose_sel_low (first 133 bits) to bit array
|
||||
component low_bits = Num2Bits(compressed_bit_len);
|
||||
low_bits.in <== compressed_disclose_sel[0];
|
||||
|
||||
// Convert disclose_sel_high (next 133 bits) to bit array
|
||||
component high_bits = Num2Bits(compressed_bit_len);
|
||||
high_bits.in <== compressed_disclose_sel[1];
|
||||
|
||||
// Combine the bit arrays (little-endian format)
|
||||
for(var i = 0; i < compressed_bit_len; i++){
|
||||
disclose_sel[i] <== low_bits.out[i];
|
||||
}
|
||||
for(var i = 0; i < compressed_bit_len; i++){
|
||||
disclose_sel[compressed_bit_len + i] <== high_bits.out[i];
|
||||
}
|
||||
|
||||
component msg_hasher = PackBytesAndPoseidon(max_length);
|
||||
for (var i = 0; i < max_length; i++) {
|
||||
msg_hasher.in[i] <== data_padded[i];
|
||||
}
|
||||
|
||||
signal leaf <== Poseidon(2)([secret, msg_hasher.out]);
|
||||
|
||||
signal computedRoot <== BinaryMerkleRoot(nLevels)(leaf, leaf_depth, path, siblings);
|
||||
merkle_root === computedRoot;
|
||||
|
||||
component disclose_circuit = DISCLOSE_KYC(MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH, namedobTreeLevels, nameyobTreeLevels);
|
||||
|
||||
for (var i = 0; i < max_length; i++) {
|
||||
disclose_circuit.data_padded[i] <== data_padded[i];
|
||||
}
|
||||
disclose_circuit.selector_data_padded <== disclose_sel;
|
||||
disclose_circuit.forbidden_countries_list <== forbidden_countries_list;
|
||||
|
||||
disclose_circuit.ofac_name_dob_smt_leaf_key <== ofac_name_dob_smt_leaf_key;
|
||||
disclose_circuit.ofac_name_dob_smt_root <== ofac_name_dob_smt_root;
|
||||
disclose_circuit.ofac_name_dob_smt_siblings <== ofac_name_dob_smt_siblings;
|
||||
|
||||
disclose_circuit.ofac_name_yob_smt_leaf_key <== ofac_name_yob_smt_leaf_key;
|
||||
disclose_circuit.ofac_name_yob_smt_root <== ofac_name_yob_smt_root;
|
||||
disclose_circuit.ofac_name_yob_smt_siblings <== ofac_name_yob_smt_siblings;
|
||||
|
||||
disclose_circuit.selector_ofac <== selector_ofac;
|
||||
disclose_circuit.current_date <== current_date;
|
||||
disclose_circuit.majority_age_ASCII <== majority_age_ASCII;
|
||||
|
||||
var revealed_data_packed_chunk_length = computeIntChunkLength(max_length + 2 + 1);
|
||||
signal output revealedData_packed[revealed_data_packed_chunk_length] <== disclose_circuit.revealedData_packed;
|
||||
|
||||
var forbidden_countries_list_packed_chunk_length = computeIntChunkLength(MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH * country_length);
|
||||
signal output forbidden_countries_list_packed[forbidden_countries_list_packed_chunk_length] <== disclose_circuit.forbidden_countries_list_packed;
|
||||
signal output nullifier <== Poseidon(2)([secret, scope]);
|
||||
signal output attestation_id <== 4;
|
||||
}
|
||||
15
circuits/circuits/disclose/vc_and_disclose_selfrica.circom
Normal file
15
circuits/circuits/disclose/vc_and_disclose_selfrica.circom
Normal file
@@ -0,0 +1,15 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "./vc_and_disclose_kyc.circom";
|
||||
|
||||
component main {
|
||||
public [
|
||||
scope,
|
||||
merkle_root,
|
||||
ofac_name_dob_smt_root,
|
||||
ofac_name_yob_smt_root,
|
||||
user_identifier,
|
||||
current_date,
|
||||
attestation_id
|
||||
]
|
||||
} = VC_AND_DISCLOSE_KYC(40, 64, 64, 33);
|
||||
1
circuits/circuits/gcp_jwt_verifier/example_jwt_fail.txt
Normal file
1
circuits/circuits/gcp_jwt_verifier/example_jwt_fail.txt
Normal file
File diff suppressed because one or more lines are too long
@@ -6,6 +6,8 @@ include "../utils/passport/customHashers.circom";
|
||||
include "../utils/gcp_jwt/extractAndValidatePubkey.circom";
|
||||
include "../utils/gcp_jwt/verifyCertificateSignature.circom";
|
||||
include "../utils/gcp_jwt/verifyJSONFieldExtraction.circom";
|
||||
include "../utils/gcp_jwt/singleOccurance.circom";
|
||||
include "../utils/gcp_jwt/validityChecker.circom";
|
||||
include "circomlib/circuits/comparators.circom";
|
||||
include "@openpassport/zk-email-circuits/utils/array.circom";
|
||||
include "@openpassport/zk-email-circuits/utils/bytes.circom";
|
||||
@@ -53,12 +55,6 @@ template GCPJWTVerifier(
|
||||
signal input intermediate_pubkey_offset;
|
||||
signal input intermediate_pubkey_actual_size;
|
||||
|
||||
// x5c[2] - Root CA certificate
|
||||
signal input root_cert[MAX_CERT_LENGTH];
|
||||
signal input root_cert_padded_length;
|
||||
signal input root_pubkey_offset;
|
||||
signal input root_pubkey_actual_size;
|
||||
|
||||
// Public keys (extracted from certificates)
|
||||
signal input leaf_pubkey[kScaled]; // From x5c[0]
|
||||
signal input intermediate_pubkey[kScaled]; // From x5c[1]
|
||||
@@ -69,11 +65,14 @@ template GCPJWTVerifier(
|
||||
signal input leaf_signature[kScaled]; // x5c[0] signature
|
||||
signal input intermediate_signature[kScaled]; // x5c[1] signature
|
||||
|
||||
signal input leaf_validity_offset;
|
||||
signal input intermediate_validity_offset;
|
||||
signal input current_date[12];
|
||||
|
||||
// GCP spec: nonce must be 10-74 bytes decoded
|
||||
// https://cloud.google.com/confidential-computing/confidential-space/docs/connect-external-resources
|
||||
// EAT nonce (payload.eat_nonce[0])
|
||||
var MAX_EAT_NONCE_B64_LENGTH = 74; // Max length for base64url string (74 bytes decoded = 99 b64url chars)
|
||||
var MAX_EAT_NONCE_B64_LENGTH = 99; // Max length for base64url string (74 bytes decoded = 99 b64url chars)
|
||||
var MAX_EAT_NONCE_KEY_LENGTH = 10; // Length of "eat_nonce" key (without quotes)
|
||||
var EAT_NONCE_PACKED_CHUNKS = computeIntChunkLength(MAX_EAT_NONCE_B64_LENGTH);
|
||||
signal input eat_nonce_0_b64_length; // Length of base64url string
|
||||
@@ -110,12 +109,44 @@ template GCPJWTVerifier(
|
||||
signal payload[maxPayloadLength];
|
||||
payload <== jwtVerifier.payload;
|
||||
|
||||
component singleOccuranceImageDigest = SingleOccurance(maxPayloadLength, 14);
|
||||
singleOccuranceImageDigest.in <== payload;
|
||||
singleOccuranceImageDigest.word[0] <== 34; // '"'
|
||||
singleOccuranceImageDigest.word[1] <== 105; // 'i'
|
||||
singleOccuranceImageDigest.word[2] <== 109; // 'm'
|
||||
singleOccuranceImageDigest.word[3] <== 97; // 'a'
|
||||
singleOccuranceImageDigest.word[4] <== 103; // 'g'
|
||||
singleOccuranceImageDigest.word[5] <== 101; // 'e'
|
||||
singleOccuranceImageDigest.word[6] <== 95; // '_'
|
||||
singleOccuranceImageDigest.word[7] <== 100; // 'd'
|
||||
singleOccuranceImageDigest.word[8] <== 105; // 'i'
|
||||
singleOccuranceImageDigest.word[9] <== 103; // 'g'
|
||||
singleOccuranceImageDigest.word[10] <== 101; // 'e'
|
||||
singleOccuranceImageDigest.word[11] <== 115; // 's'
|
||||
singleOccuranceImageDigest.word[12] <== 116; // 't'
|
||||
singleOccuranceImageDigest.word[13] <== 34; // '"'
|
||||
|
||||
component singleOccuranceEatNonce = SingleOccurance(maxPayloadLength, 11);
|
||||
singleOccuranceEatNonce.in <== payload;
|
||||
singleOccuranceEatNonce.word[0] <== 34; // '"'
|
||||
singleOccuranceEatNonce.word[1] <== 101; // 'e'
|
||||
singleOccuranceEatNonce.word[2] <== 97; // 'a'
|
||||
singleOccuranceEatNonce.word[3] <== 116; // 't'
|
||||
singleOccuranceEatNonce.word[4] <== 95; // '_'
|
||||
singleOccuranceEatNonce.word[5] <== 110; // 'n'
|
||||
singleOccuranceEatNonce.word[6] <== 111; // 'o'
|
||||
singleOccuranceEatNonce.word[7] <== 110; // 'n'
|
||||
singleOccuranceEatNonce.word[8] <== 99; // 'c'
|
||||
singleOccuranceEatNonce.word[9] <== 101; // 'e'
|
||||
singleOccuranceEatNonce.word[10] <== 34; // '"'
|
||||
|
||||
// Extract and validate x5c[0] Public Key
|
||||
ExtractAndValidatePubkey(signatureAlgorithm, n, k, MAX_CERT_LENGTH, MAX_PUBKEY_PREFIX, MAX_PUBKEY_LENGTH)(
|
||||
leaf_cert,
|
||||
leaf_pubkey_offset,
|
||||
leaf_pubkey_actual_size,
|
||||
leaf_pubkey
|
||||
leaf_pubkey,
|
||||
leaf_cert_padded_length
|
||||
);
|
||||
|
||||
// Extract and validate x5c[1] public key
|
||||
@@ -123,7 +154,22 @@ template GCPJWTVerifier(
|
||||
intermediate_cert,
|
||||
intermediate_pubkey_offset,
|
||||
intermediate_pubkey_actual_size,
|
||||
intermediate_pubkey
|
||||
intermediate_pubkey,
|
||||
intermediate_cert_padded_length
|
||||
);
|
||||
|
||||
ValidityChecker(MAX_CERT_LENGTH)(
|
||||
leaf_cert,
|
||||
leaf_cert_padded_length,
|
||||
leaf_validity_offset,
|
||||
current_date
|
||||
);
|
||||
|
||||
ValidityChecker(MAX_CERT_LENGTH)(
|
||||
intermediate_cert,
|
||||
intermediate_cert_padded_length,
|
||||
intermediate_validity_offset,
|
||||
current_date
|
||||
);
|
||||
|
||||
// Verify x5c[0] signature using x5c[1] public key
|
||||
@@ -134,14 +180,6 @@ template GCPJWTVerifier(
|
||||
leaf_signature
|
||||
);
|
||||
|
||||
// Extract and validate x5c[2] public key
|
||||
ExtractAndValidatePubkey(signatureAlgorithm, n, k, MAX_CERT_LENGTH, MAX_PUBKEY_PREFIX, MAX_PUBKEY_LENGTH)(
|
||||
root_cert,
|
||||
root_pubkey_offset,
|
||||
root_pubkey_actual_size,
|
||||
root_pubkey
|
||||
);
|
||||
|
||||
// Verify x5c[1] signature using x5c[2] public key
|
||||
VerifyCertificateSignature(signatureAlgorithm, n, k, MAX_CERT_LENGTH)(
|
||||
intermediate_cert,
|
||||
@@ -254,4 +292,4 @@ template GCPJWTVerifier(
|
||||
image_hash_packed <== PackBytes(IMAGE_HASH_LENGTH)(image_hash_bytes);
|
||||
}
|
||||
|
||||
component main = GCPJWTVerifier(1, 120, 35);
|
||||
component main { public [current_date] } = GCPJWTVerifier(1, 120, 35);
|
||||
|
||||
@@ -99,17 +99,20 @@ template FindRealMessageLength(maxLength) {
|
||||
template CountCharOccurrences(maxLength) {
|
||||
signal input in[maxLength];
|
||||
signal input char;
|
||||
signal input endIndex; // Don't count beyond this index
|
||||
signal output count;
|
||||
|
||||
signal match[maxLength];
|
||||
signal counter[maxLength];
|
||||
signal shouldCount[maxLength];
|
||||
|
||||
match[0] <== IsEqual()([in[0], char]);
|
||||
counter[0] <== match[0];
|
||||
|
||||
for (var i = 1; i < maxLength; i++) {
|
||||
shouldCount[i] <== LessThan(log2Ceil(maxLength))([i, endIndex]);
|
||||
match[i] <== IsEqual()([in[i], char]);
|
||||
counter[i] <== counter[i-1] + match[i];
|
||||
counter[i] <== counter[i-1] + match[i] * shouldCount[i];
|
||||
}
|
||||
|
||||
count <== counter[maxLength-1];
|
||||
@@ -182,13 +185,13 @@ template JWTVerifier(
|
||||
signal period <== ItemAtIndex(maxMessageLength)(message, periodIndex);
|
||||
period === 46;
|
||||
|
||||
// Assert that period is unique
|
||||
signal periodCount <== CountCharOccurrences(maxMessageLength)(message, 46);
|
||||
periodCount === 1;
|
||||
|
||||
// Find the real message length
|
||||
signal realMessageLength <== FindRealMessageLength(maxMessageLength)(message);
|
||||
|
||||
// Assert that period is unique
|
||||
signal periodCount <== CountCharOccurrences(maxMessageLength)(message, 46, realMessageLength);
|
||||
periodCount === 1;
|
||||
|
||||
// Calculate the length of the Base64 encoded header and payload
|
||||
signal b64HeaderLength <== periodIndex;
|
||||
signal b64PayloadLength <== realMessageLength - b64HeaderLength - 1;
|
||||
|
||||
@@ -26,6 +26,7 @@ interface CertificateInfo {
|
||||
publicKey: forge.pki.rsa.PublicKey;
|
||||
pubkeyOffset: number;
|
||||
pubkeyLength: number;
|
||||
validityOffset: number;
|
||||
signature: Buffer;
|
||||
cert: forge.pki.Certificate;
|
||||
}
|
||||
@@ -56,6 +57,19 @@ function parseCertificate(certDer: Buffer): CertificateInfo {
|
||||
|
||||
const pubkeyLength = pubkeyDer.length > 256 ? pubkeyDer.length - 1 : pubkeyDer.length;
|
||||
|
||||
// Locate validity sequence within TBS to provide `validity_offset` for the circuit
|
||||
const validityAsn1 = (tbsAsn1 as forge.asn1.Asn1).value[4];
|
||||
if (typeof validityAsn1 === 'string') {
|
||||
throw new Error('Expected ASN.1 object for validity sequence, got string');
|
||||
}
|
||||
const validityDer = forge.asn1.toDer(validityAsn1);
|
||||
const validityHex = Buffer.from(validityDer.getBytes(), 'binary').toString('hex');
|
||||
const validityOffsetHex = tbsHex.indexOf(validityHex);
|
||||
if (validityOffsetHex === -1) {
|
||||
throw new Error('Could not find validity sequence in TBS certificate DER encoding');
|
||||
}
|
||||
const validityOffset = validityOffsetHex / 2;
|
||||
|
||||
// Validate TBS certificate size before padding
|
||||
if (tbsBytes.length > MAX_CERT_LENGTH) {
|
||||
throw new Error(
|
||||
@@ -86,6 +100,7 @@ function parseCertificate(certDer: Buffer): CertificateInfo {
|
||||
console.log(` TBS padded length: ${paddedLength} bytes`);
|
||||
console.log(` Public key offset (in TBS): ${pubkeyOffset}`);
|
||||
console.log(` Public key length: ${pubkeyLength} bytes`);
|
||||
console.log(` Validity offset (in TBS): ${validityOffset}`);
|
||||
console.log(` Signature length: ${signature.length} bytes`);
|
||||
|
||||
return {
|
||||
@@ -95,6 +110,7 @@ function parseCertificate(certDer: Buffer): CertificateInfo {
|
||||
publicKey,
|
||||
pubkeyOffset,
|
||||
pubkeyLength,
|
||||
validityOffset,
|
||||
signature,
|
||||
cert,
|
||||
};
|
||||
@@ -179,6 +195,18 @@ function bufferToByteArray(buffer: Buffer, maxLength: number): string[] {
|
||||
return arr;
|
||||
}
|
||||
|
||||
function getCurrentDateDigitsYYMMDDHHMMSS(): string[] {
|
||||
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('');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
@@ -357,6 +385,10 @@ async function main() {
|
||||
imageDigestCharCodes[i] = imageDigest.charCodeAt(i);
|
||||
}
|
||||
|
||||
// Current UTC date (YYMMDDHHMMSS) for validity checks
|
||||
const currentDateDigits = getCurrentDateDigitsYYMMDDHHMMSS();
|
||||
console.log(`[INFO] Current UTC date (YYMMDDHHMMSS): ${currentDateDigits.join('')}`);
|
||||
|
||||
// Build circuit inputs
|
||||
const circuitInputs = {
|
||||
// JWT inputs
|
||||
@@ -369,18 +401,17 @@ async function main() {
|
||||
leaf_cert_padded_length: leafCert.paddedLength.toString(),
|
||||
leaf_pubkey_offset: leafCert.pubkeyOffset.toString(),
|
||||
leaf_pubkey_actual_size: leafCert.pubkeyLength.toString(),
|
||||
leaf_validity_offset: leafCert.validityOffset.toString(),
|
||||
|
||||
// x5c[1] - Intermediate certificate
|
||||
intermediate_cert: bufferToByteArray(intermediateCert.derPadded, MAX_CERT_LENGTH),
|
||||
intermediate_cert_padded_length: intermediateCert.paddedLength.toString(),
|
||||
intermediate_pubkey_offset: intermediateCert.pubkeyOffset.toString(),
|
||||
intermediate_pubkey_actual_size: intermediateCert.pubkeyLength.toString(),
|
||||
intermediate_validity_offset: intermediateCert.validityOffset.toString(),
|
||||
|
||||
// x5c[2] - Root certificate
|
||||
root_cert: bufferToByteArray(rootCert.derPadded, MAX_CERT_LENGTH),
|
||||
root_cert_padded_length: rootCert.paddedLength.toString(),
|
||||
root_pubkey_offset: rootCert.pubkeyOffset.toString(),
|
||||
root_pubkey_actual_size: rootCert.pubkeyLength.toString(),
|
||||
// Current date for validity checks
|
||||
current_date: currentDateDigits,
|
||||
|
||||
// Public keys (chunked for RSA circuit)
|
||||
leaf_pubkey: pubkeyToChunks(leafCert.publicKey),
|
||||
@@ -398,9 +429,6 @@ async function main() {
|
||||
eat_nonce_0_key_offset: eatNonce0KeyOffset.toString(),
|
||||
eat_nonce_0_value_offset: eatNonce0ValueOffset.toString(),
|
||||
|
||||
// EAT nonce[1] (circuit will extract value directly from payload)
|
||||
eat_nonce_1_b64_length: eatNonce1Base64url.length.toString(),
|
||||
|
||||
// Container image digest (circuit will extract value directly from payload)
|
||||
image_digest_length: imageDigest.length.toString(),
|
||||
image_digest_key_offset: imageDigestKeyOffset.toString(),
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pragma circom 2.1.9;
|
||||
include "../register_kyc.circom";
|
||||
|
||||
component main = REGISTER_KYC();
|
||||
57
circuits/circuits/register/register_kyc.circom
Normal file
57
circuits/circuits/register/register_kyc.circom
Normal file
@@ -0,0 +1,57 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "circomlib/circuits/bitify.circom";
|
||||
include "circomlib/circuits/babyjub.circom";
|
||||
include "../utils/kyc/constants.circom";
|
||||
include "../utils/passport/customHashers.circom";
|
||||
include "../utils/kyc/verifySignature.circom";
|
||||
include "circomlib/circuits/eddsaPoseidon.circom";
|
||||
|
||||
template REGISTER_KYC() {
|
||||
var max_length = KYC_MAX_LENGTH();
|
||||
var country_length = COUNTRY_LENGTH();
|
||||
var id_number_length = ID_NUMBER_LENGTH();
|
||||
var idNumberIdx = ID_NUMBER_INDEX();
|
||||
|
||||
signal input data_padded[max_length];
|
||||
|
||||
signal input s;
|
||||
signal input R[2];
|
||||
signal input pubKey[2];
|
||||
signal input secret;
|
||||
|
||||
//Check if R is on the curve
|
||||
component checkR = BabyCheck();
|
||||
checkR.x <== R[0];
|
||||
checkR.y <== R[1];
|
||||
|
||||
//Check if pubKey is on the curve
|
||||
component checkPubKey = BabyCheck();
|
||||
checkPubKey.x <== pubKey[0];
|
||||
checkPubKey.y <== pubKey[1];
|
||||
|
||||
//Calculate msg_hash
|
||||
component msg_hasher = PackBytesAndPoseidon(max_length);
|
||||
for (var i = 0; i < max_length; i++) {
|
||||
msg_hasher.in[i] <== data_padded[i];
|
||||
}
|
||||
|
||||
component verifyIdCommSig = EdDSAPoseidonVerifier();
|
||||
verifyIdCommSig.enabled <== 1;
|
||||
verifyIdCommSig.S <== s;
|
||||
verifyIdCommSig.M <== msg_hasher.out;
|
||||
verifyIdCommSig.Ax <== pubKey[0];
|
||||
verifyIdCommSig.Ay <== pubKey[1];
|
||||
verifyIdCommSig.R8x <== R[0];
|
||||
verifyIdCommSig.R8y <== R[1];
|
||||
|
||||
signal id_num[id_number_length];
|
||||
for (var i = 0; i < id_number_length; i++) {
|
||||
id_num[i] <== data_padded[idNumberIdx + i];
|
||||
}
|
||||
signal output nullifier <== PackBytesAndPoseidon(id_number_length)(id_num);
|
||||
signal output commitment <== Poseidon(2)([secret, msg_hasher.out]);
|
||||
|
||||
signal output pubkey_hash <== Poseidon(2)([verifyIdCommSig.Ax, verifyIdCommSig.Ay]);
|
||||
signal output attestation_id <== 4;
|
||||
}
|
||||
@@ -17,13 +17,13 @@ include "../utils/switcher.circom";
|
||||
// Can check for 2 bigints equality if in is sub of each chunk of those numbers
|
||||
template BigIntIsZero(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER) {
|
||||
assert(CHUNK_NUMBER >= 2);
|
||||
|
||||
|
||||
var EPSILON = 3;
|
||||
|
||||
|
||||
assert(MAX_CHUNK_SIZE + EPSILON <= 253);
|
||||
|
||||
|
||||
signal input in[CHUNK_NUMBER];
|
||||
|
||||
|
||||
signal carry[CHUNK_NUMBER - 1];
|
||||
component carryRangeChecks[CHUNK_NUMBER - 1];
|
||||
for (var i = 0; i < CHUNK_NUMBER - 1; i++){
|
||||
@@ -45,9 +45,9 @@ template BigIntIsZero(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER) {
|
||||
// Works with overflowed signed chunks
|
||||
// To handle megative values we use sign
|
||||
// Sign is var and can be changed, but it should be a problem
|
||||
// Sign change means that we can calculate for -in instead of in,
|
||||
// Sign change means that we can calculate for -in instead of in,
|
||||
// But if in % p == 0 means that -in % p == 0 too, so no exploit here
|
||||
// Problem lies in other one:
|
||||
// Problem lies in other one:
|
||||
// k - is result of div func, and can be anything (var)
|
||||
// we check k * p - in === 0
|
||||
// k * p is result of big multiplication
|
||||
@@ -71,9 +71,9 @@ template BigIntIsZero(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER) {
|
||||
template BigIntIsZeroModP(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NUMBER, CHUNK_NUMBER_MODULUS){
|
||||
signal input in[CHUNK_NUMBER];
|
||||
signal input modulus[CHUNK_NUMBER_MODULUS];
|
||||
|
||||
|
||||
var CHUNK_NUMBER_DIV = MAX_CHUNK_NUMBER - CHUNK_NUMBER_MODULUS + 1;
|
||||
|
||||
|
||||
var reduced[200] = reduce_overflow_signed_dl(CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NUMBER, MAX_CHUNK_SIZE, in);
|
||||
var div_result[2][200] = long_div_dl(CHUNK_SIZE, CHUNK_NUMBER_MODULUS, CHUNK_NUMBER_DIV - 1, reduced, modulus);
|
||||
signal sign <-- reduced[199];
|
||||
@@ -88,7 +88,7 @@ template BigIntIsZeroModP(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NU
|
||||
for (var i = 0; i < CHUNK_NUMBER_DIV; i++){
|
||||
k[i] <-- div_result[0][i];
|
||||
kRangeChecks[i] = Num2Bits(CHUNK_SIZE);
|
||||
kRangeChecks[i].in <-- k[i];
|
||||
kRangeChecks[i].in <== k[i];
|
||||
}
|
||||
|
||||
component mult;
|
||||
@@ -101,7 +101,7 @@ template BigIntIsZeroModP(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NU
|
||||
mult.in1 <== modulus;
|
||||
mult.in2 <== k;
|
||||
}
|
||||
|
||||
|
||||
component swicher[CHUNK_NUMBER];
|
||||
|
||||
component isZero = BigIntIsZero(CHUNK_SIZE, MAX_CHUNK_SIZE, MAX_CHUNK_NUMBER);
|
||||
@@ -116,5 +116,5 @@ template BigIntIsZeroModP(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NU
|
||||
for (var i = CHUNK_NUMBER; i < MAX_CHUNK_NUMBER; i++){
|
||||
isZero.in[i] <== mult.out[i];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
56
circuits/circuits/utils/gcp_jwt/dateIsLessSeconds.circom
Normal file
56
circuits/circuits/utils/gcp_jwt/dateIsLessSeconds.circom
Normal file
@@ -0,0 +1,56 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "circomlib/circuits/comparators.circom";
|
||||
|
||||
template DateIsLessSeconds() {
|
||||
signal input firstYear;
|
||||
signal input secondYear;
|
||||
|
||||
signal input firstMonth;
|
||||
signal input secondMonth;
|
||||
|
||||
signal input firstDay;
|
||||
signal input secondDay;
|
||||
|
||||
signal input firstHour;
|
||||
signal input secondHour;
|
||||
|
||||
signal input firstMinute;
|
||||
signal input secondMinute;
|
||||
|
||||
signal input firstSecond;
|
||||
signal input secondSecond;
|
||||
|
||||
signal isYearLess <== LessThan(12)([firstYear, secondYear]);
|
||||
signal isMonthLess <== LessThan(8)([firstMonth, secondMonth]);
|
||||
signal isDayLess <== LessThan(8)([firstDay, secondDay]);
|
||||
signal isHourLess <== LessThan(8)([firstHour, secondHour]);
|
||||
signal isMinuteLess <== LessThan(8)([firstMinute, secondMinute]);
|
||||
signal isSecondLess <== LessThan(8)([firstSecond, secondSecond]);
|
||||
|
||||
// ----
|
||||
|
||||
signal isYearEqual <== IsEqual()([firstYear, secondYear]);
|
||||
signal isMonthEqual <== IsEqual()([firstMonth, secondMonth]);
|
||||
signal isDayEqual <== IsEqual()([firstDay, secondDay]);
|
||||
signal isHourEqual <== IsEqual()([firstHour, secondHour]);
|
||||
signal isMinuteEqual <== IsEqual()([firstMinute, secondMinute]);
|
||||
|
||||
// ----
|
||||
|
||||
signal isYearEqualAndMonthLess <== isYearEqual * isMonthLess;
|
||||
|
||||
signal isYearAndMonthEqual <== isYearEqual * isMonthEqual;
|
||||
signal isYearAndMonthEqualAndDayLess <== isYearAndMonthEqual * isDayLess;
|
||||
|
||||
signal isYearAndMonthAndDayEqual <== isYearAndMonthEqual * isDayEqual;
|
||||
signal isYearAndMonthAndDayEqualAndHourLess <== isYearAndMonthAndDayEqual * isHourLess;
|
||||
|
||||
signal isYearAndMonthAndDayAndHourEqual <== isYearAndMonthAndDayEqual * isHourEqual;
|
||||
signal isYearAndMonthAndDayAndHourEqualAndMinuteLess <== isYearAndMonthAndDayAndHourEqual * isMinuteLess;
|
||||
|
||||
signal isYearAndMonthAndDayAndHourAndMinuteEqual <== isYearAndMonthAndDayAndHourEqual * isMinuteEqual;
|
||||
signal isYearAndMonthAndDayAndHourAndMinuteEqualAndSecondLess <== isYearAndMonthAndDayAndHourAndMinuteEqual * isSecondLess;
|
||||
|
||||
signal output out <== isYearLess + isYearEqualAndMonthLess + isYearAndMonthEqualAndDayLess + isYearAndMonthAndDayEqualAndHourLess + isYearAndMonthAndDayAndHourEqualAndMinuteLess + isYearAndMonthAndDayAndHourAndMinuteEqualAndSecondLess;
|
||||
}
|
||||
@@ -32,6 +32,7 @@ template ExtractAndValidatePubkey(
|
||||
signal input pubkey_offset;
|
||||
signal input pubkey_actual_size;
|
||||
signal input input_pubkey[kScaled];
|
||||
signal input cert_padded_length;
|
||||
|
||||
// Validate pubkey_actual_size is within bounds (prevent OOB attacks)
|
||||
component size_max_check = LessEqThan(log2Ceil(MAX_PUBKEY_LENGTH));
|
||||
@@ -45,6 +46,11 @@ template ExtractAndValidatePubkey(
|
||||
offset_min_check.in[1] <== MAX_PUBKEY_PREFIX;
|
||||
offset_min_check.out === 1;
|
||||
|
||||
component offset_max_check = LessEqThan(log2Ceil(MAX_CERT_LENGTH));
|
||||
offset_max_check.in[0] <== pubkey_offset + pubkey_actual_size;
|
||||
offset_max_check.in[1] <== cert_padded_length;
|
||||
offset_max_check.out === 1;
|
||||
|
||||
// Calculate prefix start index and net length
|
||||
signal pubkey_prefix_start_index <== pubkey_offset - MAX_PUBKEY_PREFIX;
|
||||
signal pubkey_net_length <== MAX_PUBKEY_PREFIX + pubkey_actual_size + suffixLength;
|
||||
|
||||
31
circuits/circuits/utils/gcp_jwt/singleOccurance.circom
Normal file
31
circuits/circuits/utils/gcp_jwt/singleOccurance.circom
Normal file
@@ -0,0 +1,31 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "circomlib/circuits/comparators.circom";
|
||||
|
||||
template SingleOccurance(inputLength, wordLength) {
|
||||
signal input in[inputLength];
|
||||
signal input word[wordLength];
|
||||
|
||||
// stores the matches cumulatively in a window
|
||||
signal match[inputLength - wordLength + 1][wordLength + 1];
|
||||
// equality for each letter match, used in the previous variable
|
||||
signal equals[inputLength - wordLength + 1][wordLength];
|
||||
// is equal for each window/word match
|
||||
signal matches[inputLength - wordLength + 1];
|
||||
// stores the total number of matches
|
||||
signal count[inputLength - wordLength + 2];
|
||||
|
||||
count[0] <== 0;
|
||||
|
||||
for (var i = 0; i <= inputLength - wordLength; i++) {
|
||||
match[i][0] <== 0;
|
||||
for (var j = 1; j <= wordLength; j++) {
|
||||
equals[i][j - 1] <== IsEqual()([in[i + j - 1], word[j - 1]]);
|
||||
match[i][j] <== match[i][j - 1] + equals[i][j - 1];
|
||||
}
|
||||
matches[i] <== IsEqual()([match[i][wordLength], wordLength]);
|
||||
count[i + 1] <== count[i] + matches[i];
|
||||
}
|
||||
|
||||
1 === count[inputLength - wordLength + 1];
|
||||
}
|
||||
178
circuits/circuits/utils/gcp_jwt/validityChecker.circom
Normal file
178
circuits/circuits/utils/gcp_jwt/validityChecker.circom
Normal file
@@ -0,0 +1,178 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "circomlib/circuits/comparators.circom";
|
||||
include "@openpassport/zk-email-circuits/utils/array.circom";
|
||||
include "./dateIsLessSeconds.circom";
|
||||
|
||||
template IsValidAscii() {
|
||||
signal input ascii_byte;
|
||||
signal output digit;
|
||||
|
||||
// lower bound
|
||||
component ge_zero = GreaterEqThan(8);
|
||||
ge_zero.in[0] <== ascii_byte;
|
||||
ge_zero.in[1] <== 48;
|
||||
ge_zero.out === 1;
|
||||
|
||||
// upper bound
|
||||
component le_nine = LessEqThan(8);
|
||||
le_nine.in[0] <== ascii_byte;
|
||||
le_nine.in[1] <== 57;
|
||||
le_nine.out === 1;
|
||||
|
||||
digit <== ascii_byte - 48;
|
||||
|
||||
// constrain digit < 10 to avoid overflow
|
||||
component lt_ten = LessThan(4);
|
||||
lt_ten.in[0] <== digit;
|
||||
lt_ten.in[1] <== 10;
|
||||
lt_ten.out === 1;
|
||||
}
|
||||
|
||||
/// @title ValidityChecker
|
||||
/// @notice Verifies certificate validity using UTCTime notBefore/notAfter fields
|
||||
/// @dev Assumptions and layout (hardcoded, rejects GeneralizedTime):
|
||||
/// - Expects SEQUENCE header: 0x30, length 0x1E at `validity_offset`
|
||||
/// - notBefore: tag 0x17, length 0x0d, 12 ASCII digits YYMMDDHHMMSS, trailing 'Z'
|
||||
/// - notAfter: tag 0x17, length 0x0d, 12 ASCII digits YYMMDDHHMMSS, trailing 'Z'
|
||||
/// - `validity_offset` points to the 0x30; a full 32-byte window is accessed up to
|
||||
/// `validity_offset + 31` (inclusive)
|
||||
/// - Dates are compared with `DateIsLessSeconds`; inputs are assumed to be well-formed
|
||||
/// aside from ASCII digit checks performed in-circuit.
|
||||
template ValidityChecker(MAX_CERT_LENGTH) {
|
||||
signal input cert[MAX_CERT_LENGTH];
|
||||
signal input cert_padded_length;
|
||||
signal input validity_offset;
|
||||
signal input current_date[12]; // YYMMDDHHMMSS digits
|
||||
|
||||
// ensure provided padded length does not exceed the configured maximum
|
||||
component cert_length_check = LessEqThan(log2Ceil(MAX_CERT_LENGTH));
|
||||
cert_length_check.in[0] <== cert_padded_length;
|
||||
cert_length_check.in[1] <== MAX_CERT_LENGTH;
|
||||
cert_length_check.out === 1;
|
||||
|
||||
component validity_bounds_check = LessEqThan(log2Ceil(MAX_CERT_LENGTH));
|
||||
validity_bounds_check.in[0] <== validity_offset + 1 + 2 + 13 + 2 + 13; //validity offset itself is included in the check so + 1 and not +2
|
||||
validity_bounds_check.in[1] <== cert_padded_length;
|
||||
validity_bounds_check.out === 1;
|
||||
|
||||
signal validity_offset_prefix_1 <== ItemAtIndex(MAX_CERT_LENGTH)(cert, validity_offset);
|
||||
validity_offset_prefix_1 === 0x30;
|
||||
signal validity_offset_prefix_2 <== ItemAtIndex(MAX_CERT_LENGTH)(cert, validity_offset + 1);
|
||||
validity_offset_prefix_2 === 0x1E;
|
||||
|
||||
//not before prefix
|
||||
signal not_before_prefix_1 <== ItemAtIndex(MAX_CERT_LENGTH)(cert, validity_offset + 2);
|
||||
not_before_prefix_1 === 0x17; //UTC Time + until 2048 I think? Should be safe to assume it's 2000 later in the code. Does not support generalized time
|
||||
signal not_before_prefix_2 <== ItemAtIndex(MAX_CERT_LENGTH)(cert, validity_offset + 3);
|
||||
not_before_prefix_2 === 0x0D; //13 decimal digits
|
||||
|
||||
signal not_before_offset <== validity_offset + 4;
|
||||
|
||||
signal not_before_digits_bytes[12];
|
||||
signal not_before_digits[12];
|
||||
for (var i = 0; i < 12; i++) {
|
||||
not_before_digits_bytes[i] <== ItemAtIndex(MAX_CERT_LENGTH)(cert, not_before_offset + i);
|
||||
not_before_digits[i] <== IsValidAscii()(not_before_digits_bytes[i]);
|
||||
}
|
||||
|
||||
signal not_before_suffix_1 <== ItemAtIndex(MAX_CERT_LENGTH)(cert, not_before_offset + 12);
|
||||
not_before_suffix_1 === 0x5A; //Z means UTC time
|
||||
|
||||
//not after prefix
|
||||
signal not_after_prefix_1 <== ItemAtIndex(MAX_CERT_LENGTH)(cert, not_before_offset + 12 + 1);
|
||||
not_after_prefix_1 === 0x17;
|
||||
signal not_after_prefix_2 <== ItemAtIndex(MAX_CERT_LENGTH)(cert, not_before_offset + 12 + 2);
|
||||
not_after_prefix_2 === 0x0d;
|
||||
|
||||
signal not_after_offset <== not_before_offset + 12 + 3;
|
||||
|
||||
signal not_after_digits_bytes[12];
|
||||
signal not_after_digits[12];
|
||||
for (var i = 0; i < 12; i++) {
|
||||
not_after_digits_bytes[i] <== ItemAtIndex(MAX_CERT_LENGTH)(cert, not_after_offset + i);
|
||||
not_after_digits[i] <== IsValidAscii()(not_after_digits_bytes[i]);
|
||||
}
|
||||
|
||||
signal not_after_suffix_1 <== ItemAtIndex(MAX_CERT_LENGTH)(cert, not_after_offset + 12);
|
||||
not_after_suffix_1 === 0x5A;
|
||||
|
||||
signal current_date_year <== 2000 + current_date[0] * 10 + current_date[1];
|
||||
signal current_date_month <== current_date[2] * 10 + current_date[3];
|
||||
signal current_date_day <== current_date[4] * 10 + current_date[5];
|
||||
signal current_date_hour <== current_date[6] * 10 + current_date[7];
|
||||
signal current_date_minute <== current_date[8] * 10 + current_date[9];
|
||||
signal current_date_second <== current_date[10] * 10 + current_date[11];
|
||||
|
||||
signal not_before_year <== 2000 + not_before_digits[0] * 10 + not_before_digits[1];
|
||||
signal not_before_year_valid <== LessThan(12)([not_before_year, 2050]);
|
||||
not_before_year_valid === 1;
|
||||
signal not_before_month <== not_before_digits[2] * 10 + not_before_digits[3];
|
||||
signal not_before_month_valid <== LessThan(8)([not_before_month, 13]);
|
||||
not_before_month_valid === 1;
|
||||
signal not_before_day <== not_before_digits[4] * 10 + not_before_digits[5];
|
||||
signal not_before_day_valid <== LessThan(8)([not_before_day, 32]);
|
||||
not_before_day_valid === 1;
|
||||
signal not_before_hour <== not_before_digits[6] * 10 + not_before_digits[7];
|
||||
signal not_before_hour_valid <== LessThan(8)([not_before_hour, 24]);
|
||||
not_before_hour_valid === 1;
|
||||
signal not_before_minute <== not_before_digits[8] * 10 + not_before_digits[9];
|
||||
signal not_before_minute_valid <== LessThan(8)([not_before_minute, 60]);
|
||||
not_before_minute_valid === 1;
|
||||
signal not_before_second <== not_before_digits[10] * 10 + not_before_digits[11];
|
||||
signal not_before_second_valid <== LessThan(8)([not_before_second, 60]);
|
||||
not_before_second_valid === 1;
|
||||
|
||||
signal not_after_year <== 2000 + not_after_digits[0] * 10 + not_after_digits[1];
|
||||
signal not_after_year_valid <== LessThan(12)([not_after_year, 2050]);
|
||||
not_after_year_valid === 1;
|
||||
signal not_after_month <== not_after_digits[2] * 10 + not_after_digits[3];
|
||||
signal not_after_month_valid <== LessThan(8)([not_after_month, 13]);
|
||||
not_after_month_valid === 1;
|
||||
signal not_after_day <== not_after_digits[4] * 10 + not_after_digits[5];
|
||||
signal not_after_day_valid <== LessThan(8)([not_after_day, 32]);
|
||||
not_after_day_valid === 1;
|
||||
signal not_after_hour <== not_after_digits[6] * 10 + not_after_digits[7];
|
||||
signal not_after_hour_valid <== LessThan(8)([not_after_hour, 24]);
|
||||
not_after_hour_valid === 1;
|
||||
signal not_after_minute <== not_after_digits[8] * 10 + not_after_digits[9];
|
||||
signal not_after_minute_valid <== LessThan(8)([not_after_minute, 60]);
|
||||
not_after_minute_valid === 1;
|
||||
signal not_after_second <== not_after_digits[10] * 10 + not_after_digits[11];
|
||||
signal not_after_second_valid <== LessThan(8)([not_after_second, 60]);
|
||||
not_after_second_valid === 1;
|
||||
|
||||
component not_before_check = DateIsLessSeconds();
|
||||
not_before_check.firstYear <== not_before_year;
|
||||
not_before_check.firstMonth <== not_before_month;
|
||||
not_before_check.firstDay <== not_before_day;
|
||||
not_before_check.firstHour <== not_before_hour;
|
||||
not_before_check.firstMinute <== not_before_minute;
|
||||
not_before_check.firstSecond <== not_before_second;
|
||||
|
||||
not_before_check.secondYear <== current_date_year;
|
||||
not_before_check.secondMonth <== current_date_month;
|
||||
not_before_check.secondDay <== current_date_day;
|
||||
not_before_check.secondHour <== current_date_hour;
|
||||
not_before_check.secondMinute <== current_date_minute;
|
||||
not_before_check.secondSecond <== current_date_second;
|
||||
|
||||
not_before_check.out === 1;
|
||||
|
||||
component not_after_check = DateIsLessSeconds();
|
||||
not_after_check.firstYear <== current_date_year;
|
||||
not_after_check.firstMonth <== current_date_month;
|
||||
not_after_check.firstDay <== current_date_day;
|
||||
not_after_check.firstHour <== current_date_hour;
|
||||
not_after_check.firstMinute <== current_date_minute;
|
||||
not_after_check.firstSecond <== current_date_second;
|
||||
|
||||
not_after_check.secondYear <== not_after_year;
|
||||
not_after_check.secondMonth <== not_after_month;
|
||||
not_after_check.secondDay <== not_after_day;
|
||||
not_after_check.secondHour <== not_after_hour;
|
||||
not_after_check.secondMinute <== not_after_minute;
|
||||
not_after_check.secondSecond <== not_after_second;
|
||||
|
||||
not_after_check.out === 1;
|
||||
}
|
||||
@@ -115,6 +115,13 @@ template ExtractAndVerifyJSONField(
|
||||
is_closing_bracket.in[0] <== char_after_quote;
|
||||
is_closing_bracket.in[1] <== 93; // ']'
|
||||
|
||||
// if it is an array then key_offset - value_offset must be 4 (":[") else 3 (":")
|
||||
signal difference <== 3 + is_bracket.out;
|
||||
component bindKeyWithValue = IsEqual();
|
||||
bindKeyWithValue.in[0] <== difference;
|
||||
bindKeyWithValue.in[1] <== value_offset - (key_offset + key_length);
|
||||
bindKeyWithValue.out === 1;
|
||||
|
||||
component is_comma = IsEqual();
|
||||
is_comma.in[0] <== char_after_quote;
|
||||
is_comma.in[1] <== 44; // ','
|
||||
|
||||
63
circuits/circuits/utils/kyc/babyEcdsa.circom
Normal file
63
circuits/circuits/utils/kyc/babyEcdsa.circom
Normal file
@@ -0,0 +1,63 @@
|
||||
// --------------------------------------------------
|
||||
// Source: https://github.com/cursive-team/babyjubjub-ecdsa
|
||||
// File: packages/circuits/baby-jubjub-ecdsa/baby_jubjub_ecdsa.circom
|
||||
// License: MIT
|
||||
// Author(s): cursive-team
|
||||
// Changes: no changes
|
||||
// --------------------------------------------------
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "circomlib/circuits/babyjub.circom";
|
||||
include "circomlib/circuits/bitify.circom";
|
||||
include "circomlib/circuits/escalarmulany.circom";
|
||||
/**
|
||||
* BabyJubJubECDSA
|
||||
* ====================
|
||||
*
|
||||
* Converts inputted efficient ECDSA signature to an public key. There is no
|
||||
* public key validation included. Takes in points in Twisted Edwards form
|
||||
* and uses Edwards addition and scalar multiplication. Returns computed
|
||||
* public key in Edwards form.
|
||||
*/
|
||||
template BabyJubJubECDSA() {
|
||||
var bits = 254;
|
||||
signal input s;
|
||||
signal input Tx; // T = r^-1 * R
|
||||
signal input Ty; // T is represented in Twisted Edwards form
|
||||
signal input Ux; // U = -(m * r^-1 * G)
|
||||
signal input Uy; // U is represented in Twisted Edwards form
|
||||
|
||||
signal output pubKeyX; // Represented in Twisted Edwards form
|
||||
signal output pubKeyY;
|
||||
|
||||
// bitify s
|
||||
component sBits = Num2Bits_strict();
|
||||
sBits.in <== s;
|
||||
|
||||
// check T, U are on curve
|
||||
component checkT = BabyCheck();
|
||||
checkT.x <== Tx;
|
||||
checkT.y <== Ty;
|
||||
component checkU = BabyCheck();
|
||||
checkU.x <== Ux;
|
||||
checkU.y <== Uy;
|
||||
|
||||
// sMultT = s * T
|
||||
component sMultT = EscalarMulAny(bits);
|
||||
var i;
|
||||
for (i=0; i<bits; i++) {
|
||||
sMultT.e[i] <== sBits.out[i];
|
||||
}
|
||||
sMultT.p[0] <== Tx;
|
||||
sMultT.p[1] <== Ty;
|
||||
|
||||
// pubKey = sMultT + U
|
||||
component pubKey = BabyAdd();
|
||||
pubKey.x1 <== sMultT.out[0];
|
||||
pubKey.y1 <== sMultT.out[1];
|
||||
pubKey.x2 <== Ux;
|
||||
pubKey.y2 <== Uy;
|
||||
|
||||
pubKeyX <== pubKey.xout;
|
||||
pubKeyY <== pubKey.yout;
|
||||
}
|
||||
101
circuits/circuits/utils/kyc/constants.circom
Normal file
101
circuits/circuits/utils/kyc/constants.circom
Normal file
@@ -0,0 +1,101 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
function COUNTRY_INDEX() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function COUNTRY_LENGTH() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
function ID_TYPE_INDEX() {
|
||||
return COUNTRY_INDEX() + COUNTRY_LENGTH();
|
||||
}
|
||||
|
||||
function ID_TYPE_LENGTH() {
|
||||
return 27;
|
||||
}
|
||||
|
||||
function ID_NUMBER_INDEX() {
|
||||
return ID_TYPE_INDEX() + ID_TYPE_LENGTH();
|
||||
}
|
||||
|
||||
function ID_NUMBER_LENGTH() {
|
||||
return 32;
|
||||
}
|
||||
|
||||
function ISSUANCE_DATE_INDEX() {
|
||||
return ID_NUMBER_INDEX() + ID_NUMBER_LENGTH();
|
||||
}
|
||||
|
||||
function ISSUANCE_DATE_LENGTH() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
function EXPIRATION_DATE_INDEX() {
|
||||
return ISSUANCE_DATE_INDEX() + ISSUANCE_DATE_LENGTH();
|
||||
}
|
||||
|
||||
function EXPIRATION_DATE_LENGTH() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
function FULL_NAME_INDEX() {
|
||||
return EXPIRATION_DATE_INDEX() + EXPIRATION_DATE_LENGTH();
|
||||
}
|
||||
|
||||
function FULL_NAME_LENGTH() {
|
||||
return 64;
|
||||
}
|
||||
|
||||
function DOB_INDEX() {
|
||||
return FULL_NAME_INDEX() + FULL_NAME_LENGTH();
|
||||
}
|
||||
|
||||
function DOB_LENGTH() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
function PHOTO_HASH_INDEX() {
|
||||
return DOB_INDEX() + DOB_LENGTH();
|
||||
}
|
||||
|
||||
function PHOTO_HASH_LENGTH() {
|
||||
return 32;
|
||||
}
|
||||
|
||||
function PHONE_NUMBER_INDEX() {
|
||||
return PHOTO_HASH_INDEX() + PHOTO_HASH_LENGTH();
|
||||
}
|
||||
|
||||
function PHONE_NUMBER_LENGTH() {
|
||||
return 12;
|
||||
}
|
||||
|
||||
function DOCUMENT_INDEX() {
|
||||
return PHONE_NUMBER_INDEX() + PHONE_NUMBER_LENGTH();
|
||||
}
|
||||
|
||||
function DOCUMENT_LENGTH() {
|
||||
return 32;
|
||||
}
|
||||
|
||||
function GENDER_INDEX() {
|
||||
return DOCUMENT_INDEX() + DOCUMENT_LENGTH();
|
||||
}
|
||||
|
||||
function GENDER_LENGTH() {
|
||||
return 6;
|
||||
}
|
||||
|
||||
function ADDRESS_INDEX() {
|
||||
return GENDER_INDEX() + GENDER_LENGTH();
|
||||
}
|
||||
|
||||
function ADDRESS_LENGTH() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
function KYC_MAX_LENGTH() {
|
||||
return ADDRESS_INDEX() + ADDRESS_LENGTH();
|
||||
}
|
||||
64
circuits/circuits/utils/kyc/date/dateIsLess.circom
Normal file
64
circuits/circuits/utils/kyc/date/dateIsLess.circom
Normal file
@@ -0,0 +1,64 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "circomlib/circuits/comparators.circom";
|
||||
|
||||
/// @title DateIsLess
|
||||
/// @notice compares two dates
|
||||
/// @param day_1 is the day of the first date
|
||||
/// @param day_2 is the day of the second date
|
||||
/// @param month_1 is the month of the first date
|
||||
/// @param month_2 is the month of the second date
|
||||
/// @param year_1 is the year of the first date
|
||||
/// @param year_2 is the year of the second date
|
||||
/// @output out is the result of the comparison
|
||||
/// @dev output is not constrained — verifier has to handle this check
|
||||
|
||||
template DateIsLessFullYear() {
|
||||
signal input day_1;
|
||||
signal input day_2;
|
||||
|
||||
signal input month_1;
|
||||
signal input month_2;
|
||||
|
||||
signal input year_1;
|
||||
signal input year_2;
|
||||
|
||||
signal output out;
|
||||
|
||||
component year_less = LessThan(14);
|
||||
year_less.in[0] <== year_1;
|
||||
year_less.in[1] <== year_2;
|
||||
signal is_year_less <== year_less.out;
|
||||
|
||||
component month_less = LessThan(4);
|
||||
month_less.in[0] <== month_1;
|
||||
month_less.in[1] <== month_2;
|
||||
signal is_month_less <== month_less.out;
|
||||
|
||||
component day_less = LessThan(5);
|
||||
day_less.in[0] <== day_1;
|
||||
day_less.in[1] <== day_2;
|
||||
signal is_day_less <== day_less.out;
|
||||
|
||||
// ----
|
||||
component year_equal = IsEqual();
|
||||
year_equal.in[0] <== year_1;
|
||||
year_equal.in[1] <== year_2;
|
||||
signal is_year_equal <== year_equal.out;
|
||||
|
||||
component month_equal = IsEqual();
|
||||
month_equal.in[0] <== month_1;
|
||||
month_equal.in[1] <== month_2;
|
||||
signal is_month_equal <== month_equal.out;
|
||||
|
||||
// ----
|
||||
signal is_year_equal_and_month_less <== (is_year_equal * is_month_less);
|
||||
signal is_year_equal_and_month_equal <== (is_year_equal * is_month_equal);
|
||||
signal is_year_equal_and_month_equal_and_day_less <== (is_year_equal_and_month_equal * is_day_less);
|
||||
|
||||
component greater_than = GreaterThan(3);
|
||||
greater_than.in[0] <== is_year_less + is_year_equal_and_month_less + is_year_equal_and_month_equal_and_day_less;
|
||||
greater_than.in[1] <== 0;
|
||||
|
||||
out <== greater_than.out;
|
||||
}
|
||||
87
circuits/circuits/utils/kyc/date/isOlderThan.circom
Normal file
87
circuits/circuits/utils/kyc/date/isOlderThan.circom
Normal file
@@ -0,0 +1,87 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "circomlib/circuits/comparators.circom";
|
||||
include "circomlib/circuits/bitify.circom";
|
||||
include "./dateIsLess.circom";
|
||||
|
||||
/// @title IsOlderThan
|
||||
/// @notice Verifies if user is older than the majority at the current date
|
||||
/// @param majorityASCII Majority user wants to prove he is older than: YYY — ASCII
|
||||
/// @param currDate Current date: YYYYMMDD — number
|
||||
/// @param birthDateASCII Birthdate: YYYYMMDD — ASCII
|
||||
/// @output out Result of the comparison
|
||||
/// @dev output is not constrained — verifier has to handle this check
|
||||
|
||||
template IsOlderThan() {
|
||||
signal input majorityASCII[3];
|
||||
signal input currDate[8];
|
||||
signal input birthDateASCII[8];
|
||||
|
||||
signal birthdateNum[8];
|
||||
signal ASCII_rotation <== 48;
|
||||
|
||||
for (var i=0; i<8; i++) {
|
||||
birthdateNum[i] <== birthDateASCII[i] - ASCII_rotation;
|
||||
}
|
||||
|
||||
signal TEN <== 10;
|
||||
signal CENTURY <== 100;
|
||||
signal MILLENIA <== 1000;
|
||||
|
||||
signal currDateMillenia <== currDate[0] * MILLENIA;
|
||||
signal currDateCentury <== currDate[1] * CENTURY;
|
||||
signal currDateDecade <== currDate[2] * TEN;
|
||||
signal currDateYear <== currDateMillenia + currDateCentury + currDateDecade + currDate[3];
|
||||
|
||||
signal birthDateMillenia <== birthdateNum[0] * MILLENIA;
|
||||
signal birthDateCentury <== birthdateNum[1] * CENTURY;
|
||||
signal birthDateDecade <== birthdateNum[2] * TEN;
|
||||
signal birthDateYear <== birthDateMillenia + birthDateCentury + birthDateDecade + birthdateNum[3];
|
||||
|
||||
// assert majority is between 0 and 999 (48-57 in ASCII)
|
||||
component lessThan[6];
|
||||
component range_check_majority[3];
|
||||
for (var i = 0; i < 6; i++) {
|
||||
lessThan[i] = LessThan(6);
|
||||
}
|
||||
for (var i = 0; i < 3; i++) {
|
||||
range_check_majority[i] = Num2Bits(6);
|
||||
range_check_majority[i].in <== majorityASCII[i];
|
||||
}
|
||||
lessThan[0].in[0] <== 47;
|
||||
lessThan[0].in[1] <== majorityASCII[0];
|
||||
lessThan[1].in[0] <== 47;
|
||||
lessThan[1].in[1] <== majorityASCII[1];
|
||||
lessThan[2].in[0] <== 47;
|
||||
lessThan[2].in[1] <== majorityASCII[2];
|
||||
|
||||
lessThan[3].in[0] <== majorityASCII[0];
|
||||
lessThan[3].in[1] <== 58;
|
||||
lessThan[4].in[0] <== majorityASCII[1];
|
||||
lessThan[4].in[1] <== 58;
|
||||
lessThan[5].in[0] <== majorityASCII[2];
|
||||
lessThan[5].in[1] <== 58;
|
||||
|
||||
signal checkLessThan[6];
|
||||
checkLessThan[0] <== lessThan[0].out;
|
||||
for (var i = 1; i < 6; i++) {
|
||||
checkLessThan[i] <== checkLessThan[i-1] * lessThan[i].out;
|
||||
}
|
||||
checkLessThan[5] === 1;
|
||||
|
||||
signal majorityNumCentury <== ( majorityASCII[0] - 48 ) * CENTURY;
|
||||
signal majorityNumDecade <== ( majorityASCII[1] - 48 ) * TEN;
|
||||
signal majorityNum <== majorityNumCentury + majorityNumDecade + ( majorityASCII[2] - 48 );
|
||||
|
||||
component is_older_than = DateIsLessFullYear();
|
||||
|
||||
is_older_than.year_1 <== birthDateYear + majorityNum;
|
||||
is_older_than.month_1 <== birthdateNum[4] * TEN + birthdateNum[5];
|
||||
is_older_than.day_1 <== birthdateNum[6] * TEN + birthdateNum[7];
|
||||
|
||||
is_older_than.year_2 <== currDateYear;
|
||||
is_older_than.month_2 <== currDate[4] * TEN + currDate[5];
|
||||
is_older_than.day_2 <== currDate[6] * TEN + currDate[7];
|
||||
|
||||
signal output out <== is_older_than.out;
|
||||
}
|
||||
49
circuits/circuits/utils/kyc/date/isValid.circom
Normal file
49
circuits/circuits/utils/kyc/date/isValid.circom
Normal file
@@ -0,0 +1,49 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "circomlib/circuits/comparators.circom";
|
||||
include "circomlib/circuits/bitify.circom";
|
||||
include "./dateIsLess.circom";
|
||||
|
||||
/// @title IsValid
|
||||
/// @notice Verifies if the passport is valid at the current date
|
||||
/// @param currDate Current date: YYYYMMDD — number
|
||||
/// @param validityDateASCII Validity date: YYYYMMDD — ASCII
|
||||
/// @output out Result of the comparison
|
||||
/// @dev output is constrained
|
||||
|
||||
template IsValidFullYear() {
|
||||
signal input current_date[8];
|
||||
signal input validity_date_ascii[8];
|
||||
|
||||
signal validity_date_num[8];
|
||||
signal ASCII_rotation <== 48;
|
||||
|
||||
for (var i = 0; i < 8; i++) {
|
||||
validity_date_num[i] <== validity_date_ascii[i] - ASCII_rotation;
|
||||
}
|
||||
|
||||
signal TEN <== 10;
|
||||
signal CENTURY <== 100;
|
||||
signal MILLENIA <== 1000;
|
||||
|
||||
signal current_date_year_millenia <== current_date[0] * MILLENIA;
|
||||
signal current_date_year_century <== current_date[1] * CENTURY;
|
||||
signal current_date_year_decade <== current_date[2] * TEN;
|
||||
signal current_date_year <== current_date_year_millenia + current_date_year_century + current_date_year_decade + current_date[3];
|
||||
|
||||
signal validity_date_year_millenia <== validity_date_num[0] * MILLENIA;
|
||||
signal validity_date_year_century <== validity_date_num[1] * CENTURY;
|
||||
signal validity_date_year_decade <== validity_date_num[2] * TEN;
|
||||
signal validity_date_year <== validity_date_year_millenia + validity_date_year_century + validity_date_year_decade + validity_date_num[3];
|
||||
|
||||
component is_valid = DateIsLessFullYear();
|
||||
is_valid.year_1 <== current_date_year;
|
||||
is_valid.month_1 <== current_date[4] * TEN + current_date[5];
|
||||
is_valid.day_1 <== current_date[6] * TEN + current_date[7];
|
||||
|
||||
is_valid.year_2 <== validity_date_year;
|
||||
is_valid.month_2 <== validity_date_num[4] * TEN + validity_date_num[5];
|
||||
is_valid.day_2 <== validity_date_num[6] * TEN + validity_date_num[7];
|
||||
|
||||
1 === is_valid.out;
|
||||
}
|
||||
126
circuits/circuits/utils/kyc/disclose/disclose.circom
Normal file
126
circuits/circuits/utils/kyc/disclose/disclose.circom
Normal file
@@ -0,0 +1,126 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "./ofac/ofac_name_dob_kyc.circom";
|
||||
include "./ofac/ofac_name_yob_kyc.circom";
|
||||
include "../../aadhaar/disclose/country_not_in_list.circom";
|
||||
include "circomlib/circuits/comparators.circom";
|
||||
include "../date/isValid.circom";
|
||||
include "../date/isOlderThan.circom";
|
||||
include "../constants.circom";
|
||||
|
||||
/// @title DISCLOSE_KYC
|
||||
/// @notice Performs comprehensive KYC verification including OFAC checks, age verification, document validity, and country restrictions
|
||||
/// @param MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH Maximum number of countries in the forbidden countries list
|
||||
/// @param name_dob_tree_levels Depth of the sparse merkle tree for OFAC name+DOB verification
|
||||
/// @param name_yob_tree_levels Depth of the sparse merkle tree for OFAC name+YOB verification
|
||||
/// @input data_padded The padded KYC data containing all document fields (country, expiration date, DOB, etc.)
|
||||
/// @input selector_data_padded Selector array to control which fields from data_padded are revealed
|
||||
/// @input forbidden_countries_list Flat array of forbidden country codes
|
||||
/// @input ofac_name_dob_smt_leaf_key Leaf key for OFAC name+DOB sparse merkle tree verification
|
||||
/// @input ofac_name_dob_smt_root Root of the OFAC name+DOB sparse merkle tree
|
||||
/// @input ofac_name_dob_smt_siblings Sibling nodes for OFAC name+DOB merkle proof
|
||||
/// @input ofac_name_yob_smt_leaf_key Leaf key for OFAC name+YOB sparse merkle tree verification
|
||||
/// @input ofac_name_yob_smt_root Root of the OFAC name+YOB sparse merkle tree
|
||||
/// @input ofac_name_yob_smt_siblings Sibling nodes for OFAC name+YOB merkle proof
|
||||
/// @input selector_ofac Binary selector to enable/disable OFAC checks (0 or 1)
|
||||
/// @input current_date Current date in YYYYMMDD format )
|
||||
/// @input majority_age_ASCII Age threshold for majority verification (ASCII encoded, 3 digits)
|
||||
/// @output forbidden_countries_list_packed Packed representation of the forbidden countries list
|
||||
/// @output revealedData_packed Packed array containing selectively revealed data fields and verification results
|
||||
/// @dev The output includes OFAC check results, age verification result, and selectively disclosed document fields
|
||||
/// @dev selector_ofac must be binary (0 or 1) and is constrained
|
||||
|
||||
template DISCLOSE_KYC(
|
||||
MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH,
|
||||
name_dob_tree_levels,
|
||||
name_yob_tree_levels
|
||||
) {
|
||||
var max_length = KYC_MAX_LENGTH();
|
||||
var country_length = COUNTRY_LENGTH();
|
||||
var country_index = COUNTRY_INDEX();
|
||||
var expiration_date_length = EXPIRATION_DATE_LENGTH();
|
||||
var expiration_date_index = EXPIRATION_DATE_INDEX();
|
||||
var dob_length = DOB_LENGTH();
|
||||
var dob_index = DOB_INDEX();
|
||||
|
||||
|
||||
signal input data_padded[max_length];
|
||||
signal input selector_data_padded[max_length];
|
||||
|
||||
signal input forbidden_countries_list[MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH * country_length];
|
||||
|
||||
signal input ofac_name_dob_smt_leaf_key;
|
||||
signal input ofac_name_dob_smt_root;
|
||||
signal input ofac_name_dob_smt_siblings[name_dob_tree_levels];
|
||||
|
||||
signal input ofac_name_yob_smt_leaf_key;
|
||||
signal input ofac_name_yob_smt_root;
|
||||
signal input ofac_name_yob_smt_siblings[name_yob_tree_levels];
|
||||
|
||||
signal input selector_ofac;
|
||||
signal input current_date[8];
|
||||
signal input majority_age_ASCII[3];
|
||||
|
||||
selector_ofac * (selector_ofac - 1) === 0;
|
||||
|
||||
|
||||
signal validity_ASCII[8];
|
||||
for (var i = 0; i < expiration_date_length ; i++) {
|
||||
validity_ASCII[i] <== data_padded[expiration_date_index + i];
|
||||
}
|
||||
IsValidFullYear()(current_date, validity_ASCII);
|
||||
|
||||
signal birth_date_ASCII[8];
|
||||
for (var i = 0; i < dob_length ; i++) {
|
||||
birth_date_ASCII[i] <== data_padded[dob_index + i];
|
||||
}
|
||||
|
||||
component is_older_than = IsOlderThan();
|
||||
is_older_than.majorityASCII <== majority_age_ASCII;
|
||||
is_older_than.currDate <== current_date;
|
||||
is_older_than.birthDateASCII <== birth_date_ASCII;
|
||||
|
||||
component ofac_name_dob_circuit = OFAC_NAME_DOB_KYC(name_dob_tree_levels);
|
||||
ofac_name_dob_circuit.data_padded <== data_padded;
|
||||
ofac_name_dob_circuit.smt_leaf_key <== ofac_name_dob_smt_leaf_key;
|
||||
ofac_name_dob_circuit.smt_root <== ofac_name_dob_smt_root;
|
||||
ofac_name_dob_circuit.smt_siblings <== ofac_name_dob_smt_siblings;
|
||||
|
||||
component ofac_name_yob_circuit = OFAC_NAME_YOB_KYC(name_yob_tree_levels);
|
||||
ofac_name_yob_circuit.data_padded <== data_padded;
|
||||
ofac_name_yob_circuit.smt_leaf_key <== ofac_name_yob_smt_leaf_key;
|
||||
ofac_name_yob_circuit.smt_root <== ofac_name_yob_smt_root;
|
||||
ofac_name_yob_circuit.smt_siblings <== ofac_name_yob_smt_siblings;
|
||||
|
||||
signal revealed_data[max_length + 2 + 1];
|
||||
for (var i = 0; i < max_length; i++) {
|
||||
revealed_data[i] <== data_padded[i] * selector_data_padded[i];
|
||||
}
|
||||
|
||||
signal majority_age_100 <== (majority_age_ASCII[0] - 48) * 100;
|
||||
signal majority_age_10 <== (majority_age_ASCII[1] - 48) * 10;
|
||||
signal majority_age_1 <== majority_age_ASCII[2] - 48;
|
||||
signal majority_age <== majority_age_100 + majority_age_10 + majority_age_1;
|
||||
|
||||
component lessThan256 = LessThan(8);
|
||||
lessThan256.in[0] <== majority_age;
|
||||
lessThan256.in[1] <== 255;
|
||||
lessThan256.out === 1;
|
||||
|
||||
revealed_data[max_length] <== ofac_name_dob_circuit.ofacCheckResult * selector_ofac;
|
||||
revealed_data[max_length + 1] <== ofac_name_yob_circuit.ofacCheckResult * selector_ofac;
|
||||
revealed_data[max_length + 2] <== is_older_than.out * majority_age;
|
||||
|
||||
component country_not_in_list_circuit = CountryNotInList(MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH);
|
||||
|
||||
for (var i = 0; i < country_length; i++) {
|
||||
country_not_in_list_circuit.country[i] <== data_padded[country_index + i];
|
||||
}
|
||||
country_not_in_list_circuit.forbidden_countries_list <== forbidden_countries_list;
|
||||
|
||||
var chunkLength = computeIntChunkLength(MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH * country_length);
|
||||
signal output forbidden_countries_list_packed[chunkLength] <== country_not_in_list_circuit.forbidden_countries_list_packed;
|
||||
|
||||
var revealed_data_packed_chunk_length = computeIntChunkLength(max_length + 2 + 1);
|
||||
signal output revealedData_packed[revealed_data_packed_chunk_length] <== PackBytes(max_length + 2 + 1)(revealed_data);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "circomlib/circuits/poseidon.circom";
|
||||
include "../../../crypto/merkle-trees/smt.circom";
|
||||
include "../../../passport/customHashers.circom";
|
||||
include "../../constants.circom";
|
||||
|
||||
template OFAC_NAME_DOB_KYC(n_levels) {
|
||||
var max_length = KYC_MAX_LENGTH();
|
||||
signal input data_padded[max_length];
|
||||
|
||||
signal input smt_leaf_key;
|
||||
signal input smt_root;
|
||||
signal input smt_siblings[n_levels];
|
||||
|
||||
var name_length = FULL_NAME_LENGTH();
|
||||
var name_index = FULL_NAME_INDEX();
|
||||
//name hash
|
||||
component name_hash = PackBytesAndPoseidon(name_length);
|
||||
for (var i = name_index; i < name_index + name_length; i++) {
|
||||
name_hash.in[i - name_index] <== data_padded[i];
|
||||
}
|
||||
|
||||
var dob_length = DOB_LENGTH();
|
||||
var dob_index = DOB_INDEX();
|
||||
// Dob hash
|
||||
component dob_hash = Poseidon(dob_length);
|
||||
|
||||
for(var i = 0; i < dob_length; i++) {
|
||||
dob_hash.inputs[i] <== data_padded[dob_index + i];
|
||||
}
|
||||
|
||||
// NameDob hash
|
||||
signal name_dob_hash <== Poseidon(2)([dob_hash.out, name_hash.out]);
|
||||
|
||||
signal output ofacCheckResult <== SMTVerify(n_levels)(name_dob_hash, smt_leaf_key, smt_root, smt_siblings, 0);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "circomlib/circuits/poseidon.circom";
|
||||
include "../../../crypto/merkle-trees/smt.circom";
|
||||
include "../../../passport/customHashers.circom";
|
||||
include "../../constants.circom";
|
||||
|
||||
template OFAC_NAME_YOB_KYC(n_levels) {
|
||||
var max_length = KYC_MAX_LENGTH();
|
||||
signal input data_padded[max_length];
|
||||
|
||||
signal input smt_leaf_key;
|
||||
signal input smt_root;
|
||||
signal input smt_siblings[n_levels];
|
||||
|
||||
var name_length = FULL_NAME_LENGTH();
|
||||
var name_index = FULL_NAME_INDEX();
|
||||
|
||||
//name hash
|
||||
component name_hash = PackBytesAndPoseidon(name_length);
|
||||
for (var i = name_index; i < name_index + name_length; i++) {
|
||||
name_hash.in[i - name_index] <== data_padded[i];
|
||||
}
|
||||
|
||||
// YoB hash
|
||||
component yob_hash = Poseidon(4);
|
||||
//yob is the first 4 bytes of the dob
|
||||
var yob_index = DOB_INDEX();
|
||||
for(var i = 0; i < 4; i++) {
|
||||
yob_hash.inputs[i] <== data_padded[yob_index + i];
|
||||
}
|
||||
|
||||
signal name_yob_hash <== Poseidon(2)([yob_hash.out, name_hash.out]);
|
||||
|
||||
signal output ofacCheckResult <== SMTVerify(n_levels)(name_yob_hash, smt_leaf_key, smt_root, smt_siblings, 0);
|
||||
}
|
||||
211
circuits/circuits/utils/kyc/verifySignature.circom
Normal file
211
circuits/circuits/utils/kyc/verifySignature.circom
Normal file
@@ -0,0 +1,211 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
// include "circomlib/circuits/poseidon.circom";
|
||||
// include "circomlib/circuits/escalarmulfix.circom";
|
||||
// include "circomlib/circuits/bitify.circom";
|
||||
// include "circomlib/circuits/compconstant.circom";
|
||||
// include "circomlib/circuits/comparators.circom";
|
||||
// include "@openpassport/zk-email-circuits/lib/bigint.circom";
|
||||
// include "./babyEcdsa.circom";
|
||||
// include "../crypto/bigInt/bigInt.circom";
|
||||
|
||||
// template VERIFY_KYC_SIGNATURE(){
|
||||
|
||||
// signal input s;
|
||||
// signal input msg_hash_limbs[4];
|
||||
// signal input R;
|
||||
// signal input pubKey;
|
||||
|
||||
// var SUBGROUP_ORDER = 2736030358979909402780800718157159386076813972158567259200215660948447373041; //(251 bits)
|
||||
// var BASE8[2] = [
|
||||
// 5299619240641551281634865583518297030282874472190772894086521144482721001553,
|
||||
// 16950150798460657717958625567821834550301663161624707787222815936182638968203
|
||||
// ];
|
||||
|
||||
// component computes2bits = Num2Bits_strict();
|
||||
// computes2bits.in <== s;
|
||||
|
||||
// // asserts s is a 251 bit number
|
||||
// for(var i = 0; i < 3; i++){
|
||||
// computes2bits.out[251 + i] === 0;
|
||||
// }
|
||||
|
||||
|
||||
// // Check s should be less than SUBGROUPT_ORDER - 1
|
||||
// component compConst = CompConstant(SUBGROUP_ORDER - 1);
|
||||
// compConst.in <== computes2bits.out;
|
||||
// compConst.out === 0;
|
||||
|
||||
// // Check if s is 0
|
||||
// signal is_s_zero <== IsZero()(s);
|
||||
// is_s_zero === 0;
|
||||
|
||||
|
||||
// signal scalar_mod[4];
|
||||
// // SUBGROUP ORDER in limbs
|
||||
// scalar_mod[0] <== 7454187305358665457;
|
||||
// scalar_mod[1] <== 12339561404529962506;
|
||||
// scalar_mod[2] <== 3965992003123030795;
|
||||
// scalar_mod[3] <== 435874783350371333;
|
||||
|
||||
// signal minus_1[4];
|
||||
// minus_1[0] <== scalar_mod[0] - 1;
|
||||
// minus_1[1] <== scalar_mod[1];
|
||||
// minus_1[2] <== scalar_mod[2];
|
||||
// minus_1[3] <== scalar_mod[3];
|
||||
|
||||
// //range check on r_inv[i] < 2 ^ 64
|
||||
// component range_check_r_inv_bits[4];
|
||||
// for(var i = 0; i < 4; i++){
|
||||
// range_check_r_inv_bits[i] = Num2Bits(64);
|
||||
// range_check_r_inv_bits[i].in <== r_inv[i];
|
||||
// }
|
||||
|
||||
// signal zero[4];
|
||||
// for(var i = 0; i < 4; i++){
|
||||
// zero[i] <== 0;
|
||||
// }
|
||||
|
||||
// signal one[4];
|
||||
// one[0] <== 1;
|
||||
// one[1] <== 0;
|
||||
// one[2] <== 0;
|
||||
// one[3] <== 0;
|
||||
|
||||
// // Check if r_inv is in the range of 0 to SUBGROUP_ORDER - 1
|
||||
// component range_check_r_inv = BigRangeCheck(64,4);
|
||||
// range_check_r_inv.value <== r_inv;
|
||||
// range_check_r_inv.lowerBound <== zero;
|
||||
// range_check_r_inv.upperBound <== scalar_mod;
|
||||
// range_check_r_inv.out === 1;
|
||||
|
||||
// // Check r_inv + neg_r_inv === 0
|
||||
// component neg_r_inv = BabyScalarMul();
|
||||
// neg_r_inv.in1 <== r_inv;
|
||||
// neg_r_inv.in2 <== minus_1;
|
||||
|
||||
// // Checking if Rx * r_inv == identity
|
||||
// signal Rx_bits[254];
|
||||
// component bit_decompose = Num2Bits_strict();
|
||||
// bit_decompose.in <== Rx;
|
||||
// Rx_bits <== bit_decompose.out;
|
||||
// signal Rx_limbs[4];
|
||||
// component bits2Num[4];
|
||||
|
||||
// // Convert Rx_bits (little-endian) to 4 LE limbs
|
||||
// for (var i = 0; i < 3; i++) {
|
||||
// bits2Num[i] = Bits2Num(64);
|
||||
// for (var j = 0; j < 64; j++) {
|
||||
// bits2Num[i].in[j] <== Rx_bits[i * 64 + j];
|
||||
// }
|
||||
// Rx_limbs[i] <== bits2Num[i].out;
|
||||
// }
|
||||
|
||||
// bits2Num[3] = Bits2Num(62);
|
||||
// for (var i = 192; i < 254; i++) {
|
||||
// bits2Num[3].in[i - 192] <== Rx_bits[i];
|
||||
// }
|
||||
// Rx_limbs[3] <== bits2Num[3].out;
|
||||
|
||||
// // See if r_inv * Rx == identity
|
||||
// component identity = BabyScalarMul();
|
||||
// identity.in1 <== r_inv;
|
||||
// identity.in2 <== Rx_limbs;
|
||||
|
||||
// identity.out[0] === 1;
|
||||
// identity.out[1] === 0;
|
||||
// identity.out[2] === 0;
|
||||
// identity.out[3] === 0;
|
||||
|
||||
// component T = EscalarMulAny(254);
|
||||
// signal r_inv_bits[256];
|
||||
// component num2bits[8];
|
||||
|
||||
// // convert r_inv limbs to bits
|
||||
// for (var i = 0; i < 4; i++){
|
||||
// num2bits[i]= Num2Bits(64);
|
||||
// num2bits[i].in <== r_inv[i];
|
||||
// for(var j = 0; j < 64; j++){
|
||||
// r_inv_bits[i * 64 +j] <== num2bits[i].out[j];
|
||||
// }
|
||||
// }
|
||||
// for(var i = 0; i < 254; i++){
|
||||
// T.e[i] <== r_inv_bits[i];
|
||||
// }
|
||||
// T.p[0] <== Rx;
|
||||
// T.p[1] <== Ry;
|
||||
|
||||
// // msg_hash % SUBORDER
|
||||
// component msgReduced = BigMultModP(64, 4, 4, 4);
|
||||
// for(var i = 0; i < 4; i++){
|
||||
// msgReduced.in1[i]<== msg_hash_limbs[i];
|
||||
// if(i == 0) {
|
||||
// msgReduced.in2[i]<== 1;
|
||||
// }
|
||||
// else{
|
||||
// msgReduced.in2[i]<== 0;
|
||||
// }
|
||||
// msgReduced.modulus[i]<== scalar_mod[i];
|
||||
// }
|
||||
|
||||
// // calculates (- r_inv * msg_hash) % SUBGROUP_ORDER
|
||||
// component neg_r_inv_msg_hash = BabyScalarMul();
|
||||
// for(var i = 0 ;i < 4 ;i++) {
|
||||
// neg_r_inv_msg_hash.in1[i] <== neg_r_inv.out[i];
|
||||
// neg_r_inv_msg_hash.in2[i] <== msgReduced.mod[i];
|
||||
// }
|
||||
|
||||
// signal neg_r_inv_msg_hash_bits[256];
|
||||
|
||||
// // convert neg_r_inv_msg_hash limbs to bits
|
||||
// for (var i = 0; i < 4; i++){
|
||||
// num2bits[4 + i]= Num2Bits(64);
|
||||
// num2bits[4 + i].in <== neg_r_inv_msg_hash.out[i];
|
||||
// for(var j = 0; j < 64; j++){
|
||||
// neg_r_inv_msg_hash_bits[i * 64 +j] <== num2bits[4 + i].out[j];
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// component mulFix = EscalarMulFix(254, BASE8);
|
||||
// for (var i = 0; i < 254; i++) {
|
||||
// mulFix.e[i] <== neg_r_inv_msg_hash_bits[i];
|
||||
// }
|
||||
|
||||
// component ecdsa = BabyJubJubECDSA();
|
||||
// ecdsa.Tx <== T.out[0];
|
||||
// ecdsa.Ty <== T.out[1];
|
||||
// ecdsa.Ux <== mulFix.out[0];
|
||||
// ecdsa.Uy <== mulFix.out[1];
|
||||
// ecdsa.s <== s;
|
||||
|
||||
// ecdsa.pubKeyX === pubKeyX;
|
||||
// ecdsa.pubKeyY === pubKeyY;
|
||||
// }
|
||||
|
||||
|
||||
// template BabyScalarMul(){
|
||||
// signal input in1[4];
|
||||
// signal input in2[4];
|
||||
|
||||
// signal output out[4];
|
||||
|
||||
// signal scalar_mod[4];
|
||||
// //2736030358979909402780800718157159386076813972158567259200215660948447373041(SUBGROUP ORDER)
|
||||
// scalar_mod[0] <== 7454187305358665457;
|
||||
// scalar_mod[1] <== 12339561404529962506;
|
||||
// scalar_mod[2] <== 3965992003123030795;
|
||||
// scalar_mod[3] <== 435874783350371333;
|
||||
|
||||
// component mulmod = BigMultModP(64,4,4,4);
|
||||
|
||||
// for(var i = 0; i < 4; i++){
|
||||
// mulmod.in1[i]<== in1[i];
|
||||
// mulmod.in2[i]<== in2[i];
|
||||
// mulmod.modulus[i]<== scalar_mod[i];
|
||||
// }
|
||||
// for(var i = 0; i < 4 ; i++){
|
||||
// out[i] <== mulmod.mod[i];
|
||||
// }
|
||||
|
||||
// }
|
||||
@@ -12,6 +12,7 @@
|
||||
"build-gcp-jwt-verifier": "bash scripts/build/build_gcp_jwt_verifier.sh",
|
||||
"build-register": "bash scripts/build/build_register_circuits.sh",
|
||||
"build-register-id": "bash scripts/build/build_register_circuits_id.sh",
|
||||
"build-register-selfrica": "bash scripts/build/build_register_selfrica.sh",
|
||||
"download": "bash scripts/server/download_circuits_from_AWS.sh",
|
||||
"format": "prettier --write .",
|
||||
"install-circuits": "yarn workspaces focus @selfxyz/circuits",
|
||||
@@ -22,6 +23,7 @@
|
||||
"test-custom-hasher": "yarn test-base 'tests/other_circuits/custom_hasher.test.ts' --exit",
|
||||
"test-disclose": "yarn test-base 'tests/disclose/vc_and_disclose.test.ts' --exit",
|
||||
"test-disclose-aadhaar": "yarn test-base 'tests/disclose/vc_and_disclose_aadhaar.test.ts' --exit",
|
||||
"test-disclose-kyc": "yarn test-base 'tests/disclose/vc_and_disclose_kyc.test.ts' --exit",
|
||||
"test-disclose-id": "yarn test-base 'tests/disclose/vc_and_disclose_id.test.ts' --exit",
|
||||
"test-dsc": "yarn test-base --max-old-space-size=51200 'tests/dsc/dsc.test.ts' --exit",
|
||||
"test-ecdsa": "yarn test-base 'tests/utils/ecdsa.test.ts' --exit",
|
||||
@@ -33,6 +35,7 @@
|
||||
"test-qr-extractor": "yarn test-base 'tests/other_circuits/qrdata_extractor.test.ts' --exit",
|
||||
"test-register": "yarn test-base --max-old-space-size=40960 'tests/register/register.test.ts' --exit",
|
||||
"test-register-aadhaar": "yarn test-base 'tests/register/register_aadhaar.test.ts' --exit",
|
||||
"test-register-kyc": "yarn test-base 'tests/register/register_kyc.test.ts' --exit",
|
||||
"test-register-id": "yarn test-base --max-old-space-size=40960 'tests/register_id/register_id.test.ts' --exit",
|
||||
"test-rsa": "yarn test-base 'tests/utils/rsaPkcs1v1_5.test.ts' --exit",
|
||||
"test-rsa-pss": "yarn test-base 'tests/utils/rsapss.test.ts' --exit"
|
||||
@@ -52,7 +55,7 @@
|
||||
"@zk-email/zk-regex-circom": "^1.2.1",
|
||||
"@zk-kit/binary-merkle-root.circom": "npm:@selfxyz/binary-merkle-root.circom@^0.0.1",
|
||||
"@zk-kit/circuits": "^1.0.0-beta",
|
||||
"anon-aadhaar-circuits": "https://github.com/selfxyz/anon-aadhaar.git#commit=1b9efa501cff3cf25dc260b060bf611229e316a4&workspace=@anon-aadhaar/circuits",
|
||||
"anon-aadhaar-circuits": "npm:@selfxyz/aa-circuits@^0.0.1",
|
||||
"asn1": "^0.2.6",
|
||||
"asn1.js": "^5.4.1",
|
||||
"asn1js": "^3.0.5",
|
||||
|
||||
@@ -15,9 +15,10 @@ OUTPUT_DIR="build/${CIRCUIT_TYPE}"
|
||||
# Define circuits and their configurations
|
||||
# format: name:poweroftau:build_flag
|
||||
CIRCUITS=(
|
||||
"vc_and_disclose:18:true"
|
||||
"vc_and_disclose_id:18:true"
|
||||
"vc_and_disclose_aadhaar:18:true"
|
||||
# "vc_and_disclose:20:true"
|
||||
# "vc_and_disclose_id:20:true"
|
||||
# "vc_and_disclose_aadhaar:20:true"
|
||||
"vc_and_disclose_selfrica:17:true"
|
||||
)
|
||||
|
||||
build_circuits "$CIRCUIT_TYPE" "$OUTPUT_DIR" "${CIRCUITS[@]}"
|
||||
|
||||
24
circuits/scripts/build/build_register_selfrica.sh
Executable file
24
circuits/scripts/build/build_register_selfrica.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
source "scripts/build/common.sh"
|
||||
|
||||
# Set environment (change this value as needed)
|
||||
# ENV="prod"
|
||||
ENV="staging"
|
||||
|
||||
echo -e "${GREEN}Building register circuits for $ENV environment${NC}"
|
||||
|
||||
# Circuit-specific configurations
|
||||
CIRCUIT_TYPE="register"
|
||||
OUTPUT_DIR="build/${CIRCUIT_TYPE}"
|
||||
|
||||
# Define circuits and their configurations
|
||||
# format: name:poweroftau:build_flag
|
||||
CIRCUITS=(
|
||||
"register_selfrica:15:true"
|
||||
)
|
||||
|
||||
build_circuits "$CIRCUIT_TYPE" "$OUTPUT_DIR" "${CIRCUITS[@]}"
|
||||
|
||||
echo -e "${GREEN}Register circuits build completed for $ENV environment!${NC}"
|
||||
echo -e "${YELLOW}Generated files are located in: contracts/verifiers/local/${ENV}/${CIRCUIT_TYPE}/${NC}"
|
||||
1
circuits/tests/consts/ofac/nameAndDobKycSMT.json
Normal file
1
circuits/tests/consts/ofac/nameAndDobKycSMT.json
Normal file
File diff suppressed because one or more lines are too long
1
circuits/tests/consts/ofac/nameAndDobPersonaSMT.json
Normal file
1
circuits/tests/consts/ofac/nameAndDobPersonaSMT.json
Normal file
File diff suppressed because one or more lines are too long
1
circuits/tests/consts/ofac/nameAndDobSelfricaSMT.json
Normal file
1
circuits/tests/consts/ofac/nameAndDobSelfricaSMT.json
Normal file
File diff suppressed because one or more lines are too long
1
circuits/tests/consts/ofac/nameAndYobKycSMT.json
Normal file
1
circuits/tests/consts/ofac/nameAndYobKycSMT.json
Normal file
File diff suppressed because one or more lines are too long
1
circuits/tests/consts/ofac/nameAndYobPersonaSMT.json
Normal file
1
circuits/tests/consts/ofac/nameAndYobPersonaSMT.json
Normal file
File diff suppressed because one or more lines are too long
1
circuits/tests/consts/ofac/nameAndYobSelfricaSMT.json
Normal file
1
circuits/tests/consts/ofac/nameAndYobSelfricaSMT.json
Normal file
File diff suppressed because one or more lines are too long
@@ -5,18 +5,23 @@ import path from 'path';
|
||||
import assert from 'assert';
|
||||
import { formatInput } from '@selfxyz/common/utils/circuits/generateInputs';
|
||||
import { unpackReveal } from '@selfxyz/common/utils/circuits/formatOutputs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createSelector, extractField } from '@selfxyz/common/utils/aadhaar/constants';
|
||||
import { prepareAadhaarDiscloseTestData } from '@selfxyz/common';
|
||||
import { SMT } from '@openpassport/zk-kit-smt';
|
||||
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
|
||||
import { poseidon2 } from 'poseidon-lite';
|
||||
import nameAndDobAadhaarjson from '../consts/ofac/nameAndDobAadhaarSMT.json' with { type: 'json' };
|
||||
import nameAndYobAadhaarjson from '../consts/ofac/nameAndYobAadhaarSMT.json' with { type: 'json' };
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const nameAndDobAadhaarjson = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '../consts/ofac/nameAndDobAadhaarSMT.json'), 'utf8')
|
||||
);
|
||||
const nameAndYobAadhaarjson = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '../consts/ofac/nameAndYobAadhaarSMT.json'), 'utf8')
|
||||
);
|
||||
|
||||
// const privateKeyPath = path.join(__dirname, '../../node_modules/anon-aadhaar-circuits/assets/testPrivateKey.pem');
|
||||
|
||||
// Dynamically resolve the anon-aadhaar-circuits package location
|
||||
function resolvePackagePath(packageName: string, subpath: string): string {
|
||||
|
||||
299
circuits/tests/disclose/vc_and_disclose_kyc.test.ts
Normal file
299
circuits/tests/disclose/vc_and_disclose_kyc.test.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
import { wasm as wasmTester } from 'circom_tester';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
NON_OFAC_DUMMY_INPUT,
|
||||
OFAC_DUMMY_INPUT,
|
||||
KYC_MAX_LENGTH,
|
||||
serializeKycData,
|
||||
} from '@selfxyz/common';
|
||||
import { SMT } from '@openpassport/zk-kit-smt';
|
||||
import { poseidon2 } from 'poseidon-lite';
|
||||
import { unpackReveal } from '@selfxyz/common/utils/circuits/formatOutputs.js';
|
||||
import { deepEqual } from 'assert';
|
||||
import { expect } from 'chai';
|
||||
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
|
||||
import { generateKycDiscloseInput } from '@selfxyz/common/utils/kyc/generateInputs';
|
||||
import { KycField } from '@selfxyz/common/utils/kyc/constants';
|
||||
import fs from 'fs';
|
||||
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Load KYC OFAC trees at module level
|
||||
const nameAndDobKycjson = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '../consts/ofac/nameAndDobKycSMT.json'), 'utf8')
|
||||
);
|
||||
const nameAndYobKycjson = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '../consts/ofac/nameAndYobKycSMT.json'), 'utf8')
|
||||
);
|
||||
|
||||
// Create SMTs at module level
|
||||
const namedob_smt = new SMT(poseidon2, true);
|
||||
namedob_smt.import(nameAndDobKycjson as any);
|
||||
|
||||
const nameyob_smt = new SMT(poseidon2, true);
|
||||
nameyob_smt.import(nameAndYobKycjson as any);
|
||||
|
||||
// Helper function to compute chunk length (matches computeIntChunkLength in circuit)
|
||||
const computeChunkLength = (dataLength: number): number => {
|
||||
return Math.ceil(dataLength / 31);
|
||||
};
|
||||
|
||||
describe('VC_AND_DISCLOSE KYC Circuit Tests', () => {
|
||||
let circuit: any;
|
||||
let tree: LeanIMT;
|
||||
|
||||
const maxLength = KYC_MAX_LENGTH;
|
||||
const chunkLength = computeChunkLength(KYC_MAX_LENGTH + 2 + 1);
|
||||
|
||||
// Helper function to extract revealed data packed array
|
||||
const getRevealedDataPacked = async (witness: any): Promise<string[]> => {
|
||||
// circuit.getOutput with the array length returns all elements 0 to length-1
|
||||
const revealedData = await circuit.getOutput(witness, [`revealedData_packed[${chunkLength}]`]);
|
||||
return Array.from({ length: chunkLength }, (_, i) =>
|
||||
revealedData[`revealedData_packed[${i}]`].toString()
|
||||
);
|
||||
};
|
||||
|
||||
before(async function () {
|
||||
this.timeout(0);
|
||||
|
||||
tree = new LeanIMT((a, b) => poseidon2([a, b]), []);
|
||||
|
||||
circuit = await wasmTester(
|
||||
path.join(__dirname, '../../circuits/disclose/vc_and_disclose_kyc.circom'),
|
||||
{
|
||||
verbose: true,
|
||||
logOutput: true,
|
||||
include: [
|
||||
'node_modules',
|
||||
'node_modules/@zk-kit/binary-merkle-root.circom/src',
|
||||
'node_modules/circomlib/circuits',
|
||||
],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should compile and load the circuit', async function () {
|
||||
this.timeout(0);
|
||||
expect(circuit).to.not.be.undefined;
|
||||
});
|
||||
|
||||
it('should verify for correct Circuit Input and output', async function () {
|
||||
this.timeout(0);
|
||||
const input = generateKycDiscloseInput(
|
||||
false,
|
||||
namedob_smt,
|
||||
nameyob_smt,
|
||||
tree as any,
|
||||
false,
|
||||
'0',
|
||||
'1234567890',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
'1234'
|
||||
);
|
||||
const witness = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(witness);
|
||||
});
|
||||
|
||||
it('should fail for invalid msg ascii', async function () {
|
||||
this.timeout(0);
|
||||
const input = generateKycDiscloseInput(
|
||||
false,
|
||||
namedob_smt,
|
||||
nameyob_smt,
|
||||
tree as any,
|
||||
false,
|
||||
'0',
|
||||
'1234567890',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
'1234'
|
||||
);
|
||||
|
||||
input.data_padded[4] = '9999999';
|
||||
try {
|
||||
const witness = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(witness);
|
||||
throw new Error('Circuit verified for invalid msg byte ascii');
|
||||
} catch (e) {
|
||||
const errMsg = e?.message || e?.toString?.() || '';
|
||||
if (!errMsg.includes('Num2Bits')) {
|
||||
throw new Error(`Expected error message to include "Num2Bits", but got:\n${errMsg}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should return 0 for an OFAC person', async function () {
|
||||
this.timeout(0);
|
||||
const input = generateKycDiscloseInput(
|
||||
true,
|
||||
namedob_smt,
|
||||
nameyob_smt,
|
||||
tree as any,
|
||||
true,
|
||||
'0',
|
||||
'1234567890',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
'1234'
|
||||
);
|
||||
const witness = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
const revealedData_packed = await getRevealedDataPacked(witness);
|
||||
const revealedDataUnpacked = unpackReveal(revealedData_packed, 'id');
|
||||
const ofac_results = revealedDataUnpacked.slice(maxLength, maxLength + 2);
|
||||
|
||||
deepEqual(ofac_results, ['\x00', '\x00']);
|
||||
});
|
||||
|
||||
it('should return 1 for a non OFAC person', async function () {
|
||||
this.timeout(0);
|
||||
const input = generateKycDiscloseInput(
|
||||
false,
|
||||
namedob_smt,
|
||||
nameyob_smt,
|
||||
tree as any,
|
||||
true,
|
||||
'0',
|
||||
'1234567890',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
'1234'
|
||||
);
|
||||
const witness = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
const revealedData_packed = await getRevealedDataPacked(witness);
|
||||
const revealedDataUnpacked = unpackReveal(revealedData_packed, 'id');
|
||||
const ofac_results = revealedDataUnpacked.slice(maxLength, maxLength + 2);
|
||||
|
||||
deepEqual(ofac_results, ['\x01', '\x01']);
|
||||
});
|
||||
|
||||
it('should return revealed data that matches the actual data', async function () {
|
||||
this.timeout(0);
|
||||
|
||||
const fieldsToReveal: KycField[] = [
|
||||
'COUNTRY',
|
||||
'ID_TYPE',
|
||||
'ID_NUMBER',
|
||||
'ISSUANCE_DATE',
|
||||
'EXPIRY_DATE',
|
||||
'FULL_NAME',
|
||||
'DOB',
|
||||
'PHOTO_HASH',
|
||||
'PHONE_NUMBER',
|
||||
'DOCUMENT',
|
||||
'GENDER',
|
||||
'ADDRESS',
|
||||
];
|
||||
const input = generateKycDiscloseInput(
|
||||
false,
|
||||
namedob_smt,
|
||||
nameyob_smt,
|
||||
tree as any,
|
||||
true,
|
||||
'0',
|
||||
'1234567890',
|
||||
fieldsToReveal,
|
||||
undefined,
|
||||
18,
|
||||
true,
|
||||
'1234'
|
||||
);
|
||||
const witness = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
const revealedData_packed = await getRevealedDataPacked(witness);
|
||||
const revealedDataUnpacked = unpackReveal(revealedData_packed, 'id');
|
||||
|
||||
const serializedData = Buffer.from(serializeKycData(NON_OFAC_DUMMY_INPUT), 'utf8');
|
||||
const serializedArray = Array.from(serializedData);
|
||||
|
||||
for (let i = 0; i < Math.min(serializedArray.length, maxLength); i++) {
|
||||
const expectedByte = serializedArray[i];
|
||||
const expectedChar = String.fromCharCode(expectedByte);
|
||||
const revealedChar = revealedDataUnpacked[i];
|
||||
const revealedByte = revealedChar.charCodeAt(0);
|
||||
expect(revealedByte).to.equal(
|
||||
expectedByte,
|
||||
`Mismatch at position ${i}: expected '${expectedChar}' (${expectedByte}) but got '${revealedChar}' (${revealedByte})`
|
||||
);
|
||||
}
|
||||
|
||||
const ofac_results = revealedDataUnpacked.slice(maxLength, maxLength + 2);
|
||||
deepEqual(ofac_results, ['\x01', '\x01']);
|
||||
|
||||
const age_result_byte = revealedDataUnpacked[maxLength + 2].charCodeAt(0);
|
||||
expect(age_result_byte).to.equal(18);
|
||||
});
|
||||
|
||||
it('should return revealed data that matches the actual data for OFAC person', async function () {
|
||||
this.timeout(0);
|
||||
|
||||
const fieldsToReveal: KycField[] = [
|
||||
'COUNTRY',
|
||||
'ID_TYPE',
|
||||
'ID_NUMBER',
|
||||
'ISSUANCE_DATE',
|
||||
'EXPIRY_DATE',
|
||||
'FULL_NAME',
|
||||
'DOB',
|
||||
'PHOTO_HASH',
|
||||
'PHONE_NUMBER',
|
||||
'DOCUMENT',
|
||||
'GENDER',
|
||||
'ADDRESS',
|
||||
];
|
||||
const input = generateKycDiscloseInput(
|
||||
true,
|
||||
namedob_smt,
|
||||
nameyob_smt,
|
||||
tree as any,
|
||||
true,
|
||||
'0',
|
||||
'1234567890',
|
||||
fieldsToReveal,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
'1234'
|
||||
);
|
||||
|
||||
const witness = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
const revealedData_packed = await getRevealedDataPacked(witness);
|
||||
const revealedDataUnpacked = unpackReveal(revealedData_packed, 'id');
|
||||
|
||||
const serializedData = Buffer.from(serializeKycData(OFAC_DUMMY_INPUT), 'utf8');
|
||||
const serializedArray = Array.from(serializedData);
|
||||
|
||||
for (let i = 0; i < Math.min(serializedArray.length, maxLength); i++) {
|
||||
const expectedByte = serializedArray[i];
|
||||
const expectedChar = String.fromCharCode(expectedByte);
|
||||
const revealedChar = revealedDataUnpacked[i];
|
||||
const revealedByte = revealedChar.charCodeAt(0);
|
||||
expect(revealedByte).to.equal(
|
||||
expectedByte,
|
||||
`Mismatch at position ${i}: expected '${expectedChar}' (${expectedByte}) but got '${revealedChar}' (${revealedByte})`
|
||||
);
|
||||
}
|
||||
|
||||
const ofac_results = revealedDataUnpacked.slice(maxLength, maxLength + 2);
|
||||
deepEqual(ofac_results, ['\x00', '\x00']);
|
||||
|
||||
const age_result_byte = revealedDataUnpacked[maxLength + 2].charCodeAt(0);
|
||||
expect(age_result_byte).to.equal(0);
|
||||
});
|
||||
});
|
||||
158
circuits/tests/register/register_kyc.test.ts
Normal file
158
circuits/tests/register/register_kyc.test.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { expect } from 'chai';
|
||||
import { wasm as wasmTester } from 'circom_tester';
|
||||
import path from 'path';
|
||||
import { packBytesAndPoseidon } from '@selfxyz/common/utils/hash';
|
||||
import { poseidon2 } from 'poseidon-lite';
|
||||
import { generateMockKycRegisterInput } from '@selfxyz/common/utils/kyc/generateInputs.js';
|
||||
import { KycRegisterInput } from '@selfxyz/common/utils/kyc/types';
|
||||
import { KYC_ID_NUMBER_INDEX, KYC_ID_NUMBER_LENGTH } from '@selfxyz/common/utils/kyc/constants';
|
||||
|
||||
describe('REGISTER KYC Circuit Tests', () => {
|
||||
let circuit: any;
|
||||
let input: KycRegisterInput;
|
||||
|
||||
before(async function () {
|
||||
this.timeout(0);
|
||||
input = await generateMockKycRegisterInput(null, true, undefined);
|
||||
circuit = await wasmTester(
|
||||
path.join(__dirname, '../../circuits/register/instances/register_selfrica.circom'),
|
||||
{
|
||||
verbose: true,
|
||||
logOutput: true,
|
||||
include: ['node_modules'],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should compile and load the circuit', async function () {
|
||||
this.timeout(0);
|
||||
expect(circuit).to.not.be.undefined;
|
||||
});
|
||||
|
||||
it('should generate the correct input', async function () {
|
||||
this.timeout(0);
|
||||
const w = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(w);
|
||||
});
|
||||
|
||||
it('should generate the correct nullifier and commitment', async function () {
|
||||
this.timeout(0);
|
||||
|
||||
let idnumber = input.data_padded.slice(
|
||||
KYC_ID_NUMBER_INDEX,
|
||||
KYC_ID_NUMBER_INDEX + KYC_ID_NUMBER_LENGTH
|
||||
);
|
||||
const nullifier = packBytesAndPoseidon(idnumber.map((x) => Number(x)));
|
||||
const commitment = poseidon2([
|
||||
input.secret,
|
||||
packBytesAndPoseidon(input.data_padded.map((x) => Number(x))),
|
||||
]);
|
||||
|
||||
const w = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(w);
|
||||
const calnullifier = (await circuit.getOutput(w, ['nullifier'])).nullifier;
|
||||
const calcommitment = (await circuit.getOutput(w, ['commitment'])).commitment;
|
||||
expect(nullifier.toString()).to.be.equal(calnullifier);
|
||||
expect(commitment.toString()).to.be.equal(calcommitment);
|
||||
});
|
||||
|
||||
it('should not verify if the signature is invalid', async function () {
|
||||
this.timeout(0);
|
||||
input.s = BigInt(input.s) + BigInt(1);
|
||||
try {
|
||||
const w = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(w);
|
||||
expect.fail('Expected an error but none was thrown.');
|
||||
} catch (error) {
|
||||
expect(error.message).to.include('Assert Failed');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail if data is tampered', async function () {
|
||||
this.timeout(0);
|
||||
input = await generateMockKycRegisterInput(null, true, undefined);
|
||||
input.data_padded[5] = Number(input.data_padded[5]) + 1;
|
||||
try {
|
||||
const w = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(w);
|
||||
expect.fail('Expected an error but none was thrown.');
|
||||
} catch (error) {
|
||||
expect(error.message).to.include('Assert Failed');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail if data is not bytes', async function () {
|
||||
this.timeout(0);
|
||||
input = await generateMockKycRegisterInput(null, true, undefined);
|
||||
input.data_padded[5] = 8000;
|
||||
try {
|
||||
const w = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(w);
|
||||
expect.fail('Expected an error but none was thrown.');
|
||||
} catch (error) {
|
||||
expect(error.message).to.include('Assert Failed');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail if s is greater than subgroup order', async function () {
|
||||
this.timeout(0);
|
||||
input.s = BigInt(
|
||||
'2736030358979909402780800718157159386076813972158567259200215660948447373041'
|
||||
);
|
||||
try {
|
||||
const w = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(w);
|
||||
expect.fail('Expected an error but none was thrown.');
|
||||
} catch (error) {
|
||||
expect(error.message).to.include('Assert Failed');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail if s is 0', async function () {
|
||||
this.timeout(0);
|
||||
input = await generateMockKycRegisterInput(null, true, undefined);
|
||||
input.s = BigInt(0);
|
||||
try {
|
||||
const w = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(w);
|
||||
expect.fail('Expected an error but none was thrown.');
|
||||
} catch (error) {
|
||||
expect(error.message).to.include('Assert Failed');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail if R is not on the curve', async function () {
|
||||
this.timeout(0);
|
||||
input = await generateMockKycRegisterInput(null, true, undefined);
|
||||
//go beyond the suborder
|
||||
input.R[0] = BigInt(
|
||||
BigInt('9736030358979909402780800718157159386076813972158567259200215660948447373049') + 1n
|
||||
);
|
||||
input.R[1] = BigInt(1);
|
||||
try {
|
||||
const w = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(w);
|
||||
expect.fail('Expected an error but none was thrown.');
|
||||
} catch (error) {
|
||||
expect(error.message).to.include('BabyCheck');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail if pubKey is not on the curve', async function () {
|
||||
this.timeout(0);
|
||||
input = await generateMockKycRegisterInput(null, true, undefined);
|
||||
input.pubKey[0] = BigInt(
|
||||
'2736030358979909402780800718157159386076813972158567259200215660948447373049'
|
||||
);
|
||||
input.pubKey[1] = BigInt(
|
||||
'2736030358979909402780800718157159386076813972158567259200215660948447373049'
|
||||
);
|
||||
try {
|
||||
const w = await circuit.calculateWitness(input);
|
||||
await circuit.checkConstraints(w);
|
||||
expect.fail('Expected an error but none was thrown.');
|
||||
} catch (error) {
|
||||
expect(error.message).to.include('BabyCheck');
|
||||
}
|
||||
});
|
||||
});
|
||||
119
circuits/tests/utils/kyc/date/date.test.ts
Normal file
119
circuits/tests/utils/kyc/date/date.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { expect } from 'chai';
|
||||
import { wasm as wasmTester } from 'circom_tester';
|
||||
import * as path from 'path';
|
||||
|
||||
describe('date', async () => {
|
||||
let circuit;
|
||||
|
||||
before(async () => {
|
||||
circuit = await wasmTester(path.join(__dirname, 'is_valid.test.circom'), {
|
||||
include: ['node_modules', 'node_modules/@zk-kit/binary-merkle-root.circom/src'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true if the year is less', async () => {
|
||||
const inputs = {
|
||||
current_date: [1, 9, 9, 4, 0, 4, 1, 2],
|
||||
validity_date_ascii: '19950412'.split('').map((x) => x.charCodeAt(0)),
|
||||
};
|
||||
|
||||
console.log(inputs);
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
expect(witness[0]).to.equal(1n);
|
||||
});
|
||||
|
||||
it('should return false if the year is greater', async () => {
|
||||
const inputs = {
|
||||
current_date: [1, 9, 9, 6, 0, 4, 1, 2],
|
||||
validity_date_ascii: '19950412'.split('').map((x) => x.charCodeAt(0)),
|
||||
};
|
||||
|
||||
try {
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
throw new Error('should return false if the year is greater: FAILED');
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
}
|
||||
});
|
||||
|
||||
it('should return true if the year is equal and month is less', async () => {
|
||||
const inputs = {
|
||||
current_date: [1, 9, 9, 6, 0, 3, 1, 2],
|
||||
validity_date_ascii: '19960412'.split('').map((x) => x.charCodeAt(0)),
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
expect(witness[0]).to.equal(1n);
|
||||
});
|
||||
|
||||
it('should return false if the year is equal and month is greater', async () => {
|
||||
const inputs = {
|
||||
current_date: [1, 9, 9, 6, 0, 5, 1, 2],
|
||||
validity_date_ascii: '19960412'.split('').map((x) => x.charCodeAt(0)),
|
||||
};
|
||||
|
||||
try {
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
throw new Error('should return false if the year is equal and month is greater: FAILED');
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
}
|
||||
});
|
||||
|
||||
it('should return true if the year is equal and month is equal and day is less', async () => {
|
||||
const inputs = {
|
||||
current_date: [1, 9, 9, 6, 0, 4, 0, 2],
|
||||
validity_date_ascii: '19960412'.split('').map((x) => x.charCodeAt(0)),
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
expect(witness[0]).to.equal(1n);
|
||||
});
|
||||
|
||||
it('should return false if the year is equal and month is equal and day is greater', async () => {
|
||||
const inputs = {
|
||||
current_date: [1, 9, 9, 6, 0, 4, 0, 3],
|
||||
validity_date_ascii: '19960412'.split('').map((x) => x.charCodeAt(0)),
|
||||
};
|
||||
|
||||
try {
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
throw new Error(
|
||||
'should return false if the year is equal and month is equal and day is greater: FAILED'
|
||||
);
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
}
|
||||
});
|
||||
|
||||
it('should return false if the year is equal and month is equal and day is equal', async () => {
|
||||
const inputs = {
|
||||
current_date: [1, 9, 9, 6, 0, 4, 0, 2],
|
||||
validity_date_ascii: '19960412'.split('').map((x) => x.charCodeAt(0)),
|
||||
};
|
||||
|
||||
try {
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
throw new Error(
|
||||
'should return false if the year is equal and month is equal and day is equal: FAILED'
|
||||
);
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
}
|
||||
});
|
||||
});
|
||||
60
circuits/tests/utils/kyc/date/isOlderThan.test.ts
Normal file
60
circuits/tests/utils/kyc/date/isOlderThan.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { expect } from 'chai';
|
||||
import { wasm as wasmTester } from 'circom_tester';
|
||||
import * as path from 'path';
|
||||
|
||||
describe('isOlderThan', async () => {
|
||||
let circuit;
|
||||
|
||||
before(async () => {
|
||||
circuit = await wasmTester(path.join(__dirname, 'is_older_than.test.circom'), {
|
||||
include: [
|
||||
'node_modules',
|
||||
'node_modules/@zk-kit/binary-merkle-root.circom/src',
|
||||
'node_modules/circomlib/circuits',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true if the user is older than the majority', async () => {
|
||||
console.log(['1', '9', '9', '4', '0', '4', '0', '2'].map((x) => x.charCodeAt(0)));
|
||||
const inputs = {
|
||||
majorityASCII: [48, 48, 48 + 2], //2 years old
|
||||
currDate: [1, 9, 9, 6, 0, 4, 0, 2],
|
||||
birthDateASCII: ['1', '9', '9', '4', '0', '4', '0', '1'].map((x) => x.charCodeAt(0)),
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
const output = await circuit.getOutput(witness, ['out']);
|
||||
expect(output.out).to.equal('1');
|
||||
});
|
||||
|
||||
it('should not return false if the user is younger than the majority', async () => {
|
||||
const inputs = {
|
||||
majorityASCII: [48, 48, 48 + 2], //2 years old
|
||||
currDate: [1, 9, 9, 5, 0, 4, 0, 2],
|
||||
birthDateASCII: ['1', '9', '9', '4', '0', '4', '0', '2'].map((x) => x.charCodeAt(0)),
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
const output = await circuit.getOutput(witness, ['out']);
|
||||
expect(output.out).to.equal('0');
|
||||
});
|
||||
|
||||
it('should not return true if the user birthdate is in the majority year but the current date is not', async () => {
|
||||
const inputs = {
|
||||
majorityASCII: [48, 48, 48 + 2], //2 years old
|
||||
currDate: [1, 9, 9, 6, 0, 4, 0, 1],
|
||||
birthDateASCII: ['1', '9', '9', '4', '0', '4', '0', '3'].map((x) => x.charCodeAt(0)),
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
const output = await circuit.getOutput(witness, ['out']);
|
||||
expect(output.out).to.equal('0');
|
||||
});
|
||||
});
|
||||
5
circuits/tests/utils/kyc/date/is_older_than.test.circom
Normal file
5
circuits/tests/utils/kyc/date/is_older_than.test.circom
Normal file
@@ -0,0 +1,5 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "../../../../circuits/utils/kyc/date/isOlderThan.circom";
|
||||
|
||||
component main = IsOlderThan();
|
||||
3
circuits/tests/utils/kyc/date/is_valid.test.circom
Normal file
3
circuits/tests/utils/kyc/date/is_valid.test.circom
Normal file
@@ -0,0 +1,3 @@
|
||||
include "../../../../circuits/utils/kyc/date/isValid.circom";
|
||||
|
||||
component main = IsValidFullYear();
|
||||
195
circuits/tests/utils/kyc/ofac/ofac.test.ts
Normal file
195
circuits/tests/utils/kyc/ofac/ofac.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { expect } from 'chai';
|
||||
import { wasm as wasmTester } from 'circom_tester';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
generateCircuitInputsOfac,
|
||||
NON_OFAC_DUMMY_INPUT,
|
||||
OFAC_DUMMY_INPUT,
|
||||
} from '../../../../../common/src/utils/kyc/generateInputs';
|
||||
import { serializeKycData } from '../../../../../common/src/utils/kyc/types';
|
||||
import { SMT } from '@openpassport/zk-kit-smt';
|
||||
import { poseidon2 } from 'poseidon-lite';
|
||||
import nameAndDobjson from '../../../consts/ofac/nameAndDobKycSMT.json';
|
||||
import nameAndYobjson from '../../../consts/ofac/nameAndYobKycSMT.json';
|
||||
|
||||
describe('OFAC - Name and DOB match', async function () {
|
||||
this.timeout(10000);
|
||||
let circuit;
|
||||
let namedob_smt = new SMT(poseidon2, true);
|
||||
let proofLevel = 2;
|
||||
|
||||
before(async () => {
|
||||
circuit = await wasmTester(path.join(__dirname, 'ofac_name_dob_kyc.test.circom'), {
|
||||
include: [
|
||||
'node_modules',
|
||||
'./node_modules/@zk-kit/binary-merkle-root.circom/src',
|
||||
'./node_modules/circomlib/circuits',
|
||||
],
|
||||
});
|
||||
|
||||
namedob_smt.import(nameAndDobjson);
|
||||
});
|
||||
|
||||
it('should compile and load the circuit', async () => {
|
||||
expect(circuit).to.not.be.undefined;
|
||||
});
|
||||
|
||||
it('should return 0 if the person is in the ofac list', async () => {
|
||||
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
|
||||
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, namedob_smt, proofLevel);
|
||||
const inputs = {
|
||||
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
|
||||
...ofacInputs,
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
const ofacCheckResult = (await circuit.getOutput(witness, ['ofacCheckResult'])).ofacCheckResult;
|
||||
expect(ofacCheckResult).to.equal('0');
|
||||
});
|
||||
|
||||
it('should return 1 if the person is not in the ofac list', async () => {
|
||||
const dummy_kyc_input = serializeKycData(NON_OFAC_DUMMY_INPUT);
|
||||
const ofacInputs = generateCircuitInputsOfac(NON_OFAC_DUMMY_INPUT, namedob_smt, proofLevel);
|
||||
const inputs = {
|
||||
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
|
||||
...ofacInputs,
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
const ofacCheckResult = (await circuit.getOutput(witness, ['ofacCheckResult'])).ofacCheckResult;
|
||||
expect(ofacCheckResult).to.equal('1');
|
||||
});
|
||||
|
||||
it('should return 0 if the internal computed merkle root is wrong (wrong leaf key)', async () => {
|
||||
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
|
||||
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, namedob_smt, proofLevel);
|
||||
const inputs = {
|
||||
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
|
||||
...ofacInputs,
|
||||
smt_leaf_key: BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString(),
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
const ofacCheckResult = (await circuit.getOutput(witness, ['ofacCheckResult'])).ofacCheckResult;
|
||||
expect(ofacCheckResult).to.equal('0');
|
||||
});
|
||||
|
||||
it('should return 0 if the internal computed merkle root is wrong (wrong siblings)', async () => {
|
||||
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
|
||||
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, namedob_smt, proofLevel);
|
||||
ofacInputs.smt_siblings[0] = BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString();
|
||||
const inputs = {
|
||||
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
|
||||
...ofacInputs,
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
const ofacCheckResult = (await circuit.getOutput(witness, ['ofacCheckResult'])).ofacCheckResult;
|
||||
expect(ofacCheckResult).to.equal('0');
|
||||
});
|
||||
|
||||
it('should return 0 if the merkle root is wrong', async () => {
|
||||
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
|
||||
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, namedob_smt, proofLevel);
|
||||
const inputs = {
|
||||
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
|
||||
...ofacInputs,
|
||||
smt_root: BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString(),
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
const ofacCheckResult = (await circuit.getOutput(witness, ['ofacCheckResult'])).ofacCheckResult;
|
||||
expect(ofacCheckResult).to.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OFAC - Name and YOB match', async function () {
|
||||
this.timeout(10000);
|
||||
let circuit;
|
||||
let nameyob_smt = new SMT(poseidon2, true);
|
||||
let proofLevel = 1;
|
||||
|
||||
before(async () => {
|
||||
circuit = await wasmTester(path.join(__dirname, 'ofac_name_yob_kyc.test.circom'), {
|
||||
include: [
|
||||
'node_modules',
|
||||
'./node_modules/@zk-kit/binary-merkle-root.circom/src',
|
||||
'./node_modules/circomlib/circuits',
|
||||
],
|
||||
});
|
||||
|
||||
nameyob_smt.import(nameAndYobjson);
|
||||
});
|
||||
|
||||
it('should compile and load the circuit', async () => {
|
||||
expect(circuit).to.not.be.undefined;
|
||||
});
|
||||
|
||||
it('should return 0 if the person is in the ofac list', async () => {
|
||||
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
|
||||
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, nameyob_smt, proofLevel);
|
||||
const inputs = {
|
||||
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
|
||||
...ofacInputs,
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
const ofacCheckResult = (await circuit.getOutput(witness, ['ofacCheckResult'])).ofacCheckResult;
|
||||
expect(ofacCheckResult).to.equal('0');
|
||||
});
|
||||
|
||||
it('should return 1 if the person is not in the ofac list', async () => {
|
||||
const dummy_kyc_input = serializeKycData(NON_OFAC_DUMMY_INPUT);
|
||||
const ofacInputs = generateCircuitInputsOfac(NON_OFAC_DUMMY_INPUT, nameyob_smt, proofLevel);
|
||||
const inputs = {
|
||||
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
|
||||
...ofacInputs,
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
const ofacCheckResult = (await circuit.getOutput(witness, ['ofacCheckResult'])).ofacCheckResult;
|
||||
expect(ofacCheckResult).to.equal('1');
|
||||
});
|
||||
|
||||
it('should return 0 if the internal computed merkle root is wrong (wrong leaf key)', async () => {
|
||||
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
|
||||
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, nameyob_smt, proofLevel);
|
||||
const inputs = {
|
||||
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
|
||||
...ofacInputs,
|
||||
smt_leaf_key: BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString(),
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
const ofacCheckResult = (await circuit.getOutput(witness, ['ofacCheckResult'])).ofacCheckResult;
|
||||
expect(ofacCheckResult).to.equal('0');
|
||||
});
|
||||
|
||||
it('should return 0 if the internal computed merkle root is wrong (wrong siblings)', async () => {
|
||||
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
|
||||
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, nameyob_smt, proofLevel);
|
||||
ofacInputs.smt_siblings[0] = BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString();
|
||||
const inputs = {
|
||||
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
|
||||
...ofacInputs,
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
const ofacCheckResult = (await circuit.getOutput(witness, ['ofacCheckResult'])).ofacCheckResult;
|
||||
expect(ofacCheckResult).to.equal('0');
|
||||
});
|
||||
|
||||
it('should return 0 if the merkle root is wrong', async () => {
|
||||
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
|
||||
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, nameyob_smt, proofLevel);
|
||||
const inputs = {
|
||||
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
|
||||
...ofacInputs,
|
||||
smt_root: BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString(),
|
||||
};
|
||||
|
||||
const witness = await circuit.calculateWitness(inputs);
|
||||
const ofacCheckResult = (await circuit.getOutput(witness, ['ofacCheckResult'])).ofacCheckResult;
|
||||
expect(ofacCheckResult).to.equal('0');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "../../../../circuits/utils/kyc/disclose/ofac/ofac_name_dob_kyc.circom";
|
||||
|
||||
component main = OFAC_NAME_DOB_KYC(64);
|
||||
@@ -0,0 +1,5 @@
|
||||
pragma circom 2.1.9;
|
||||
|
||||
include "../../../../circuits/utils/kyc/disclose/ofac/ofac_name_yob_kyc.circom";
|
||||
|
||||
component main = OFAC_NAME_YOB_KYC(64);
|
||||
@@ -43,6 +43,8 @@ export {
|
||||
IDENTITY_TREE_URL_STAGING_ID_CARD,
|
||||
ID_CARD_ATTESTATION_ID,
|
||||
PASSPORT_ATTESTATION_ID,
|
||||
AADHAAR_ATTESTATION_ID,
|
||||
KYC_ATTESTATION_ID,
|
||||
PCR0_MANAGER_ADDRESS,
|
||||
REDIRECT_URL,
|
||||
RPC_URL,
|
||||
@@ -130,3 +132,18 @@ export {
|
||||
prepareAadhaarRegisterData,
|
||||
prepareAadhaarRegisterTestData,
|
||||
} from './src/utils/aadhaar/mockData.js';
|
||||
|
||||
export {
|
||||
generateKycDiscloseInput,
|
||||
generateMockKycRegisterInput,
|
||||
NON_OFAC_DUMMY_INPUT,
|
||||
OFAC_DUMMY_INPUT,
|
||||
} from './src/utils/kyc/generateInputs.js';
|
||||
|
||||
export {
|
||||
KYC_MAX_LENGTH,
|
||||
KYC_ID_NUMBER_INDEX,
|
||||
KYC_ID_NUMBER_LENGTH,
|
||||
} from './src/utils/kyc/constants.js';
|
||||
|
||||
export { serializeKycData, KycData } from './src/utils/kyc/types.js';
|
||||
|
||||
@@ -630,6 +630,26 @@
|
||||
"types": "./dist/esm/src/utils/ofac.d.ts",
|
||||
"import": "./dist/esm/src/utils/ofac.js",
|
||||
"require": "./dist/cjs/src/utils/ofac.cjs"
|
||||
},
|
||||
"./utils/kyc/generateInputs": {
|
||||
"import": {
|
||||
"types": "./dist/esm/src/utils/kyc/generateInputs.d.ts",
|
||||
"default": "./dist/esm/src/utils/kyc/generateInputs.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/src/utils/kyc/generateInputs.d.ts",
|
||||
"default": "./dist/cjs/src/utils/kyc/generateInputs.cjs"
|
||||
}
|
||||
},
|
||||
"./utils/kyc/constants": {
|
||||
"import": {
|
||||
"types": "./dist/esm/src/utils/kyc/constants.d.ts",
|
||||
"default": "./dist/esm/src/utils/kyc/constants.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/src/utils/kyc/constants.d.ts",
|
||||
"default": "./dist/cjs/src/utils/kyc/constants.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": "./dist/cjs/index.cjs",
|
||||
@@ -667,6 +687,8 @@
|
||||
"@openpassport/zk-kit-smt": "^0.0.1",
|
||||
"@peculiar/x509": "^1.12.3",
|
||||
"@stablelib/cbor": "^2.0.1",
|
||||
"@zk-kit/baby-jubjub": "^1.0.3",
|
||||
"@zk-kit/eddsa-poseidon": "^1.1.0",
|
||||
"asn1.js": "^5.4.1",
|
||||
"asn1js": "^3.0.5",
|
||||
"axios": "^1.7.2",
|
||||
|
||||
@@ -3,6 +3,7 @@ export type document_type = 'passport' | 'id_card';
|
||||
export type hashAlgosTypes = 'sha512' | 'sha384' | 'sha256' | 'sha224' | 'sha1';
|
||||
export const AADHAAR_ATTESTATION_ID = '3';
|
||||
export const API_URL = 'https://api.self.xyz';
|
||||
export const KYC_ATTESTATION_ID = '4';
|
||||
|
||||
export const API_URL_STAGING = 'https://api.staging.self.xyz';
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ export {
|
||||
IDENTITY_TREE_URL_STAGING_ID_CARD,
|
||||
ID_CARD_ATTESTATION_ID,
|
||||
PASSPORT_ATTESTATION_ID,
|
||||
AADHAAR_ATTESTATION_ID,
|
||||
KYC_ATTESTATION_ID,
|
||||
PCR0_MANAGER_ADDRESS,
|
||||
REDIRECT_URL,
|
||||
RPC_URL,
|
||||
|
||||
@@ -11,9 +11,6 @@ async function build_aadhaar_ofac_smt() {
|
||||
|
||||
// -----PASSPORT DATA-----
|
||||
console.log(`Reading data from ${baseInputPath}`);
|
||||
const passports = JSON.parse(
|
||||
fs.readFileSync(`${baseInputPath}passports.json`) as unknown as string
|
||||
);
|
||||
|
||||
// -----Aadhaar DATA-----
|
||||
console.log('\nBuilding Aadhaar Card SMTs...');
|
||||
|
||||
@@ -22,9 +22,73 @@ import type { AadhaarData, Environment, IDDocument, OfacTree } from '../../utils
|
||||
|
||||
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
|
||||
import { SMT } from '@openpassport/zk-kit-smt';
|
||||
import { KycField } from '../kyc/constants.js';
|
||||
|
||||
export { generateCircuitInputsRegister } from './generateInputs.js';
|
||||
|
||||
// export function generateTEEInputsKycDisclose( secret: string,
|
||||
// kycData: KycData,
|
||||
// selfApp: SelfApp,
|
||||
// getTree: <T extends 'ofac' | 'commitment'>(
|
||||
// doc: DocumentCategory,
|
||||
// tree: T
|
||||
// ) => T extends 'ofac' ? OfacTree : any
|
||||
|
||||
// ) {
|
||||
|
||||
// const {generateKycInputWithOutSig} = require('../kyc/generateInputs.js');
|
||||
|
||||
// const { scope, disclosures, userId, userDefinedData, chainID } = selfApp;
|
||||
// const userIdentifierHash = calculateUserIdentifierHash(chainID, userId, userDefinedData);
|
||||
|
||||
// // Map SelfAppDisclosureConfig to KycField array
|
||||
// const mapDisclosuresToKycFields = (config: SelfAppDisclosureConfig): KycField[] => {
|
||||
// const mapping: [keyof SelfAppDisclosureConfig, KycField][] = [
|
||||
// ['issuing_state', 'ADDRESS'],
|
||||
// ['nationality', 'COUNTRY'],
|
||||
// ['name', 'FULL_NAME'],
|
||||
// ['passport_number', 'ID_NUMBER'],
|
||||
// ['date_of_birth', 'DOB'],
|
||||
// ['gender', 'GENDER'],
|
||||
// ['expiry_date', 'EXPIRY_DATE'],
|
||||
// ];
|
||||
// return mapping.filter(([key]) => config[key]).map(([_, field]) => field);
|
||||
// };
|
||||
|
||||
// const ofac_trees = getTree('kyc', 'ofac');
|
||||
// if (!ofac_trees) {
|
||||
// throw new Error('OFAC trees not loaded');
|
||||
// }
|
||||
|
||||
// if (!ofac_trees.nameAndDob || !ofac_trees.nameAndYob) {
|
||||
// throw new Error('Invalid OFAC tree structure: missing required fields');
|
||||
// }
|
||||
|
||||
// const nameAndDobSMT = new SMT(poseidon2, true);
|
||||
// const nameAndYobSMT = new SMT(poseidon2, true);
|
||||
// nameAndDobSMT.import(ofac_trees.nameAndDob);
|
||||
// nameAndYobSMT.import(ofac_trees.nameAndYob);
|
||||
|
||||
// const inputs = generateKycInputWithOutSig(
|
||||
// kycData.serializedRealData,
|
||||
// nameAndDobSMT,
|
||||
// nameAndYobSMT,
|
||||
// disclosures.ofac,
|
||||
// scope,
|
||||
// userIdentifierHash.toString(),
|
||||
// mapDisclosuresToKycFields(disclosures),
|
||||
// disclosures.excludedCountries,
|
||||
// disclosures.minimumAge
|
||||
// );
|
||||
|
||||
// return {
|
||||
// inputs,
|
||||
// circuitName: 'vc_and_disclose_kyc',
|
||||
// endpointType: selfApp.endpointType,
|
||||
// endpoint: selfApp.endpoint,
|
||||
// };
|
||||
// }
|
||||
|
||||
export function generateTEEInputsAadhaarDisclose(
|
||||
secret: string,
|
||||
aadhaarData: AadhaarData,
|
||||
@@ -175,6 +239,15 @@ export function generateTEEInputsDiscloseStateless(
|
||||
);
|
||||
return { inputs, circuitName, endpointType, endpoint };
|
||||
}
|
||||
// if (passportData.documentCategory === 'kyc') {
|
||||
// const { inputs, circuitName, endpointType, endpoint } = generateTEEInputsKycDisclose(
|
||||
// secret,
|
||||
// passportData,
|
||||
// selfApp,
|
||||
// getTree
|
||||
// );
|
||||
// return { inputs, circuitName, endpointType, endpoint };
|
||||
// }
|
||||
const { scope, disclosures, endpoint, userId, userDefinedData, chainID } = selfApp;
|
||||
const userIdentifierHash = calculateUserIdentifierHash(chainID, userId, userDefinedData);
|
||||
const scope_hash = hashEndpointWithScope(endpoint, scope);
|
||||
@@ -253,6 +326,10 @@ export async function generateTEEInputsRegister(
|
||||
return { inputs, circuitName, endpointType, endpoint };
|
||||
}
|
||||
|
||||
// if (passportData.documentCategory === 'kyc') {
|
||||
// throw new Error('Kyc does not support registration');
|
||||
// }
|
||||
|
||||
const inputs = generateCircuitInputsRegister(secret, passportData, dscTree as string);
|
||||
const circuitName = getCircuitNameFromPassportData(passportData, 'register');
|
||||
const endpointType = env === 'stg' ? 'staging_celo' : 'celo';
|
||||
|
||||
179
common/src/utils/kyc/constants.ts
Normal file
179
common/src/utils/kyc/constants.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
export const KYC_COUNTRY_INDEX = 0;
|
||||
export const KYC_COUNTRY_LENGTH = 3;
|
||||
|
||||
export const KYC_ID_TYPE_INDEX = KYC_COUNTRY_INDEX + KYC_COUNTRY_LENGTH;
|
||||
export const KYC_ID_TYPE_LENGTH = 27;
|
||||
|
||||
export const KYC_ID_NUMBER_INDEX = KYC_ID_TYPE_INDEX + KYC_ID_TYPE_LENGTH;
|
||||
export const KYC_ID_NUMBER_LENGTH = 32; // Updated: max(20, 32) = 32
|
||||
|
||||
export const KYC_ISSUANCE_DATE_INDEX = KYC_ID_NUMBER_INDEX + KYC_ID_NUMBER_LENGTH;
|
||||
export const KYC_ISSUANCE_DATE_LENGTH = 8;
|
||||
|
||||
export const KYC_EXPIRY_DATE_INDEX = KYC_ISSUANCE_DATE_INDEX + KYC_ISSUANCE_DATE_LENGTH;
|
||||
export const KYC_EXPIRY_DATE_LENGTH = 8;
|
||||
|
||||
export const KYC_FULL_NAME_INDEX = KYC_EXPIRY_DATE_INDEX + KYC_EXPIRY_DATE_LENGTH;
|
||||
export const KYC_FULL_NAME_LENGTH = 64; // Updated: max(40, 64) = 64
|
||||
|
||||
export const KYC_DOB_INDEX = KYC_FULL_NAME_INDEX + KYC_FULL_NAME_LENGTH;
|
||||
export const KYC_DOB_LENGTH = 8;
|
||||
|
||||
export const KYC_PHOTO_HASH_INDEX = KYC_DOB_INDEX + KYC_DOB_LENGTH;
|
||||
export const KYC_PHOTO_HASH_LENGTH = 32;
|
||||
|
||||
export const KYC_PHONE_NUMBER_INDEX = KYC_PHOTO_HASH_INDEX + KYC_PHOTO_HASH_LENGTH;
|
||||
export const KYC_PHONE_NUMBER_LENGTH = 12;
|
||||
|
||||
export const KYC_DOCUMENT_INDEX = KYC_PHONE_NUMBER_INDEX + KYC_PHONE_NUMBER_LENGTH;
|
||||
export const KYC_DOCUMENT_LENGTH = 32; // Updated: max(2, 32) = 32
|
||||
|
||||
export const KYC_GENDER_INDEX = KYC_DOCUMENT_INDEX + KYC_DOCUMENT_LENGTH;
|
||||
export const KYC_GENDER_LENGTH = 6;
|
||||
|
||||
export const KYC_ADDRESS_INDEX = KYC_GENDER_INDEX + KYC_GENDER_LENGTH;
|
||||
export const KYC_ADDRESS_LENGTH = 100;
|
||||
|
||||
export const KYC_MAX_LENGTH = KYC_ADDRESS_INDEX + KYC_ADDRESS_LENGTH;
|
||||
|
||||
// ------------------------------
|
||||
// Field lengths for selector bits
|
||||
// ------------------------------
|
||||
export const KYC_FIELD_LENGTHS = {
|
||||
COUNTRY: KYC_COUNTRY_LENGTH, // 3
|
||||
ID_TYPE: KYC_ID_TYPE_LENGTH, // 27
|
||||
ID_NUMBER: KYC_ID_NUMBER_LENGTH, // 32 (updated)
|
||||
ISSUANCE_DATE: KYC_ISSUANCE_DATE_LENGTH, // 8
|
||||
EXPIRY_DATE: KYC_EXPIRY_DATE_LENGTH, // 8
|
||||
FULL_NAME: KYC_FULL_NAME_LENGTH, // 64 (updated)
|
||||
DOB: KYC_DOB_LENGTH, // 8
|
||||
PHOTO_HASH: KYC_PHOTO_HASH_LENGTH, // 32
|
||||
PHONE_NUMBER: KYC_PHONE_NUMBER_LENGTH, // 12
|
||||
DOCUMENT: KYC_DOCUMENT_LENGTH, // 32 (updated)
|
||||
GENDER: KYC_GENDER_LENGTH, // 6
|
||||
ADDRESS: KYC_ADDRESS_LENGTH, // 100
|
||||
} as const;
|
||||
|
||||
// ------------------------------
|
||||
// Reveal data indices for selector bits
|
||||
// ------------------------------
|
||||
export const KYC_REVEAL_DATA_INDICES = {
|
||||
COUNTRY: 0,
|
||||
ID_TYPE: KYC_COUNTRY_LENGTH, // 3
|
||||
ID_NUMBER: KYC_ID_TYPE_INDEX + KYC_ID_TYPE_LENGTH, // 30
|
||||
ISSUANCE_DATE: KYC_ID_NUMBER_INDEX + KYC_ID_NUMBER_LENGTH, // 62 (updated)
|
||||
EXPIRY_DATE: KYC_ISSUANCE_DATE_INDEX + KYC_ISSUANCE_DATE_LENGTH, // 70 (updated)
|
||||
FULL_NAME: KYC_EXPIRY_DATE_INDEX + KYC_EXPIRY_DATE_LENGTH, // 78 (updated)
|
||||
DOB: KYC_FULL_NAME_INDEX + KYC_FULL_NAME_LENGTH, // 142 (updated)
|
||||
PHOTO_HASH: KYC_DOB_INDEX + KYC_DOB_LENGTH, // 150 (updated)
|
||||
PHONE_NUMBER: KYC_PHOTO_HASH_INDEX + KYC_PHOTO_HASH_LENGTH, // 182 (updated)
|
||||
DOCUMENT: KYC_PHONE_NUMBER_INDEX + KYC_PHONE_NUMBER_LENGTH, // 194 (updated)
|
||||
GENDER: KYC_DOCUMENT_INDEX + KYC_DOCUMENT_LENGTH, // 226 (updated)
|
||||
ADDRESS: KYC_GENDER_INDEX + KYC_GENDER_LENGTH, // 232 (updated)
|
||||
} as const;
|
||||
|
||||
// ------------------------------
|
||||
// Selector bit positions for each field
|
||||
// ------------------------------
|
||||
export const KYC_SELECTOR_BITS = {
|
||||
COUNTRY: Array.from({ length: KYC_COUNTRY_LENGTH }, (_, i) => i) as number[], // 0-2
|
||||
ID_TYPE: Array.from({ length: KYC_ID_TYPE_LENGTH }, (_, i) => i + KYC_COUNTRY_LENGTH) as number[], // 3-29
|
||||
ID_NUMBER: Array.from(
|
||||
{ length: KYC_ID_NUMBER_LENGTH },
|
||||
(_, i) => i + KYC_ID_TYPE_INDEX + KYC_ID_TYPE_LENGTH
|
||||
) as number[], // 30-61 (updated)
|
||||
ISSUANCE_DATE: Array.from(
|
||||
{ length: KYC_ISSUANCE_DATE_LENGTH },
|
||||
(_, i) => i + KYC_ID_NUMBER_INDEX + KYC_ID_NUMBER_LENGTH
|
||||
) as number[], // 62-69 (updated)
|
||||
EXPIRY_DATE: Array.from(
|
||||
{ length: KYC_EXPIRY_DATE_LENGTH },
|
||||
(_, i) => i + KYC_ISSUANCE_DATE_INDEX + KYC_ISSUANCE_DATE_LENGTH
|
||||
) as number[], // 70-77 (updated)
|
||||
FULL_NAME: Array.from(
|
||||
{ length: KYC_FULL_NAME_LENGTH },
|
||||
(_, i) => i + KYC_EXPIRY_DATE_INDEX + KYC_EXPIRY_DATE_LENGTH
|
||||
) as number[], // 78-141 (updated)
|
||||
DOB: Array.from(
|
||||
{ length: KYC_DOB_LENGTH },
|
||||
(_, i) => i + KYC_FULL_NAME_INDEX + KYC_FULL_NAME_LENGTH
|
||||
) as number[], // 142-149 (updated)
|
||||
PHOTO_HASH: Array.from(
|
||||
{ length: KYC_PHOTO_HASH_LENGTH },
|
||||
(_, i) => i + KYC_DOB_INDEX + KYC_DOB_LENGTH
|
||||
) as number[], // 150-181 (updated)
|
||||
PHONE_NUMBER: Array.from(
|
||||
{ length: KYC_PHONE_NUMBER_LENGTH },
|
||||
(_, i) => i + KYC_PHOTO_HASH_INDEX + KYC_PHOTO_HASH_LENGTH
|
||||
) as number[], // 182-193 (updated)
|
||||
DOCUMENT: Array.from(
|
||||
{ length: KYC_DOCUMENT_LENGTH },
|
||||
(_, i) => i + KYC_PHONE_NUMBER_INDEX + KYC_PHONE_NUMBER_LENGTH
|
||||
) as number[], // 194-225 (updated)
|
||||
GENDER: Array.from(
|
||||
{ length: KYC_GENDER_LENGTH },
|
||||
(_, i) => i + KYC_DOCUMENT_INDEX + KYC_DOCUMENT_LENGTH
|
||||
) as number[], // 226-231 (updated)
|
||||
ADDRESS: Array.from(
|
||||
{ length: KYC_ADDRESS_LENGTH },
|
||||
(_, i) => i + KYC_GENDER_INDEX + KYC_GENDER_LENGTH
|
||||
) as number[], // 232-331 (updated)
|
||||
} as const;
|
||||
|
||||
export type KycField = keyof typeof KYC_FIELD_LENGTHS;
|
||||
|
||||
// ------------------------------
|
||||
// Public Signals Indices
|
||||
// ------------------------------
|
||||
|
||||
export const KYC_PUBLIC_SIGNALS_ATTESTATION_ID = 0;
|
||||
|
||||
export const KYC_PUBLIC_SIGNALS_REVEALED_DATA_PACKED = 1;
|
||||
export const KYC_PUBLIC_SIGNALS_REVEALED_DATA_PACKED_LENGTH = 9;
|
||||
|
||||
export const KYC_PUBLIC_SIGNALS_FORBIDDEN_COUNTRIES_PACKED = 10;
|
||||
export const KYC_PUBLIC_SIGNALS_FORBIDDEN_COUNTRIES_PACKED_LENGTH = 4;
|
||||
|
||||
export const KYC_PUBLIC_SIGNALS_NULLIFIER = 14;
|
||||
|
||||
export const KYC_PUBLIC_SIGNALS_SCOPE = 15;
|
||||
export const KYC_PUBLIC_SIGNALS_USER_IDENTIFIER = 16;
|
||||
|
||||
export const KYC_PUBLIC_SIGNALS_CURRENT_DATE = 17;
|
||||
export const KYC_PUBLIC_SIGNALS_CURRENT_DATE_LENGTH = 8;
|
||||
|
||||
export const KYC_PUBLIC_SIGNALS_OFAC_NAME_DOB_SMT_ROOT = 25;
|
||||
export const KYC_PUBLIC_SIGNALS_OFAC_NAME_YOB_SMT_ROOT = 26;
|
||||
|
||||
// ------------------------------
|
||||
// Helper functions for selector bits
|
||||
// ------------------------------
|
||||
|
||||
export function createKycSelector(fieldsToReveal: KycField[]): [bigint, bigint] {
|
||||
const bits = Array(KYC_MAX_LENGTH).fill(0);
|
||||
|
||||
for (const field of fieldsToReveal) {
|
||||
const selectorBits = KYC_SELECTOR_BITS[field];
|
||||
for (const bit of selectorBits) {
|
||||
bits[bit] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
let lowResult = 0n;
|
||||
let highResult = 0n;
|
||||
|
||||
const splitPoint = Math.floor(KYC_MAX_LENGTH / 2);
|
||||
|
||||
for (let i = 0; i < splitPoint; i++) {
|
||||
if (bits[i]) {
|
||||
lowResult += 1n << BigInt(i);
|
||||
}
|
||||
}
|
||||
for (let i = splitPoint; i < KYC_MAX_LENGTH; i++) {
|
||||
if (bits[i]) {
|
||||
highResult += 1n << BigInt(i - splitPoint);
|
||||
}
|
||||
}
|
||||
|
||||
return [lowResult, highResult];
|
||||
}
|
||||
121
common/src/utils/kyc/ecdsa/ecdsa.ts
Normal file
121
common/src/utils/kyc/ecdsa/ecdsa.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { poseidon5 } from 'poseidon-lite';
|
||||
import { modulus } from './utils.js';
|
||||
import { addPoint, Base8, mulPointEscalar, Point, subOrder } from '@zk-kit/baby-jubjub';
|
||||
import { EdDSAPoseidon, Signature } from '@zk-kit/eddsa-poseidon';
|
||||
import { packBytesAndPoseidon } from '../../hash.js';
|
||||
|
||||
export function signEdDSA(key: bigint, msg: number[]): [Signature, Point<bigint>] {
|
||||
key = modulus(key, subOrder);
|
||||
const msgHash = BigInt(packBytesAndPoseidon(msg));
|
||||
const eddsaFactory = new EdDSAPoseidon(key.toString());
|
||||
const signature = eddsaFactory.signMessage(msgHash.toString());
|
||||
console.assert(
|
||||
verifyEdDSAZkKit(eddsaFactory.publicKey, signature, msg) == true,
|
||||
'Invalid signature'
|
||||
);
|
||||
return [signature, eddsaFactory.publicKey];
|
||||
}
|
||||
|
||||
export const verifyEdDSAZkKit = (pubkey: Point<bigint>, sig: Signature, msgArr: number[]) => {
|
||||
let msg = BigInt(packBytesAndPoseidon(msgArr));
|
||||
let challenge = poseidon5([sig.R8[0], sig.R8[1], pubkey[0], pubkey[1], msg]);
|
||||
|
||||
let S = mulPointEscalar(Base8, BigInt(sig.S));
|
||||
let c_Pk = mulPointEscalar(pubkey, modulus(challenge * 8n, subOrder));
|
||||
let R_plus_c_Pk = addPoint([BigInt(sig.R8[0]), BigInt(sig.R8[1])], c_Pk);
|
||||
let minus_R_plus_c_Pk = mulPointEscalar(R_plus_c_Pk, modulus(-1n, subOrder));
|
||||
let V_plus_minus_R_plus_c_Pk = addPoint(S, minus_R_plus_c_Pk);
|
||||
let final = mulPointEscalar(V_plus_minus_R_plus_c_Pk, 8n);
|
||||
return final[0] == 0n && final[1] == 1n;
|
||||
};
|
||||
|
||||
export function buffer2bits(buff) {
|
||||
const res = [];
|
||||
for (let i = 0; i < buff.length; i++) {
|
||||
for (let j = 0; j < 8; j++) {
|
||||
if ((buff[i] >> j) & 1) {
|
||||
res.push(1n);
|
||||
} else {
|
||||
res.push(0n);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// export const verifyEdDSA = (pubkey: Point<bigint>, sig: Signature, msgStr: string) => {
|
||||
// let msg = modulus(BigInt(packBytesAndPoseidon(msgStr.split('').map(c => c.charCodeAt(0)))), subOrder);
|
||||
|
||||
// let challenge = modulus(poseidon5([
|
||||
// sig.R[0],
|
||||
// sig.R[1],
|
||||
// pubkey[0],
|
||||
// pubkey[1],
|
||||
// msg
|
||||
// ]), subOrder);
|
||||
|
||||
// let S = mulPointEscalar(Base8, sig.s);
|
||||
// let c_Pk = mulPointEscalar(pubkey, challenge);
|
||||
// let R_plus_c_Pk = addPoint(sig.R, c_Pk);
|
||||
// let minus_R_plus_c_Pk = mulPointEscalar(R_plus_c_Pk, modulus(-1n, subOrder));
|
||||
// let V_plus_minus_R_plus_c_Pk = addPoint(S, minus_R_plus_c_Pk);
|
||||
// //for the taceo library
|
||||
// let final = mulPointEscalar(V_plus_minus_R_plus_c_Pk, 8n);
|
||||
// return final[0] == 0n && final[1] == 1n;
|
||||
// }
|
||||
|
||||
//TODO: zk-kit/baby-jubjub uses affine which involses Fr.div which makes process slower , try to implement the function using PointProjective
|
||||
|
||||
// export function signECDSA(key: bigint, msg: number[]): Signature {
|
||||
// key = modulus(key, subOrder);
|
||||
// const msgHash = getECDSAMessageHash(msg);
|
||||
// // Deterministically generate the nonce k and reduce it modulo the subgroup order
|
||||
// const k = modulus(poseidon2([msgHash, key]), subOrder);
|
||||
|
||||
// const R = mulPointEscalar(Base8, k);
|
||||
|
||||
// const kInv = modInv(k, subOrder);
|
||||
|
||||
// // Compute s = k_inv * (msg_hash + r * key) mod n
|
||||
// const s = modulus(
|
||||
// kInv * (msgHash + R[0] * key),
|
||||
// subOrder
|
||||
// );
|
||||
|
||||
// return { R, s };
|
||||
// }
|
||||
|
||||
// export function verifyECDSA(msg: number[], sig: Signature, pk: Point<bigint>): boolean {
|
||||
// const msgHash = getECDSAMessageHash(msg);
|
||||
|
||||
// const sInv = modInv(sig.s, subOrder);
|
||||
|
||||
// // u1 = msg_hash * s_inv mod n
|
||||
// const u1 = modulus((msgHash * sInv), subOrder);
|
||||
// // u2 = r * s_inv mod n
|
||||
// const u2 = modulus(sig.R[0] * sInv, subOrder);
|
||||
|
||||
// // R = u1*G + u2*pk
|
||||
|
||||
// const u1G = mulPointEscalar(Base8, u1);
|
||||
// const u2Pk = mulPointEscalar(pk, u2);
|
||||
// let R = addPoint(u1G, u2Pk);
|
||||
|
||||
// return R[0] == sig.R[0]
|
||||
|
||||
// }
|
||||
|
||||
export function verifyEffECDSA(
|
||||
s: bigint,
|
||||
T: Point<bigint>,
|
||||
U: Point<bigint>,
|
||||
pk: Point<bigint>
|
||||
): boolean {
|
||||
// Check if s*T + U == pk
|
||||
const sT = mulPointEscalar(T, s);
|
||||
const calPk = addPoint(sT, U);
|
||||
|
||||
const xvalid = calPk[0] == pk[0];
|
||||
const yvalid = calPk[1] == pk[1];
|
||||
return xvalid && yvalid == true;
|
||||
}
|
||||
75
common/src/utils/kyc/ecdsa/utils.ts
Normal file
75
common/src/utils/kyc/ecdsa/utils.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Base8, mulPointEscalar, Point, subOrder } from '@zk-kit/baby-jubjub';
|
||||
import { Signature } from '../types.js';
|
||||
import { packBytesAndPoseidon } from '../../hash.js';
|
||||
/**
|
||||
* Compute the hash of a message using the ECDSA algorithm
|
||||
* @param msg
|
||||
* @returns hash as a hex string
|
||||
*/
|
||||
export const getECDSAMessageHash = (msg: number[]): bigint => {
|
||||
const msgHash = BigInt(packBytesAndPoseidon(msg));
|
||||
return modulus(msgHash, subOrder);
|
||||
};
|
||||
|
||||
export const modulus = (a: bigint, m: bigint): bigint => {
|
||||
return ((a % m) + m) % m;
|
||||
};
|
||||
|
||||
export function modInv(a: bigint, m: bigint): bigint {
|
||||
let m0 = m;
|
||||
let y = 0n,
|
||||
x = 1n;
|
||||
|
||||
if (m === 1n) return 0n;
|
||||
|
||||
while (a > 1n) {
|
||||
const q = a / m;
|
||||
let t = m;
|
||||
|
||||
// m is remainder now
|
||||
m = a % m;
|
||||
a = t;
|
||||
t = y;
|
||||
|
||||
// Update x and y
|
||||
y = x - q * y;
|
||||
x = t;
|
||||
}
|
||||
|
||||
// Make x positive
|
||||
if (x < 0n) x += m0;
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
export function getEffECDSAArgs(
|
||||
msg: number[],
|
||||
sig: Signature
|
||||
): { T: Point<bigint>; U: Point<bigint> } {
|
||||
const msgHash = getECDSAMessageHash(msg);
|
||||
const rInv = modInv(sig.R[0], subOrder);
|
||||
|
||||
// T = R * r_inv, where R is the signature's R point
|
||||
const T = mulPointEscalar(sig.R, rInv);
|
||||
// U = G * (-r_inv * msg_hash mod n), where G is the generator
|
||||
const rInvNeg = modulus(-rInv, subOrder);
|
||||
const U = mulPointEscalar(Base8, modulus(rInvNeg * msgHash, subOrder));
|
||||
return { T, U };
|
||||
}
|
||||
|
||||
export const generateRandomsg = (): number[] => {
|
||||
const randomNumbers: number[] = Array.from({ length: 298 }, () =>
|
||||
Math.floor(Math.random() * 128)
|
||||
);
|
||||
return randomNumbers;
|
||||
};
|
||||
//TODO: Recheck the logic
|
||||
export function bigintTo64bitLimbs(x: bigint): bigint[] {
|
||||
const mask = (1n << 64n) - 1n;
|
||||
const limbs: bigint[] = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
limbs.push(x & mask);
|
||||
x >>= 64n;
|
||||
}
|
||||
return limbs;
|
||||
}
|
||||
182
common/src/utils/kyc/generateInputs.ts
Normal file
182
common/src/utils/kyc/generateInputs.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { SMT } from '@openpassport/zk-kit-smt';
|
||||
import {
|
||||
generateMerkleProof,
|
||||
generateSMTProof,
|
||||
getNameDobLeafKyc,
|
||||
getNameYobLeafKyc,
|
||||
} from '../trees.js';
|
||||
import { KycDiscloseInput, KycRegisterInput, serializeKycData, KycData } from './types.js';
|
||||
import { findIndexInTree, formatInput } from '../circuits/generateInputs.js';
|
||||
import { createKycSelector, KYC_MAX_LENGTH, KycField } from './constants.js';
|
||||
import { poseidon2 } from 'poseidon-lite';
|
||||
import { Base8, inCurve, mulPointEscalar, subOrder } from '@zk-kit/baby-jubjub';
|
||||
import { signEdDSA } from './ecdsa/ecdsa.js';
|
||||
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
|
||||
import { packBytesAndPoseidon } from '../hash.js';
|
||||
import { COMMITMENT_TREE_DEPTH } from '../../constants/constants.js';
|
||||
|
||||
export const OFAC_DUMMY_INPUT: KycData = {
|
||||
country: 'KEN',
|
||||
idType: 'NATIONAL ID',
|
||||
idNumber: '12345678901234567890123456789012', //32 digits
|
||||
issuanceDate: '20200101',
|
||||
expiryDate: '20290101',
|
||||
fullName: 'ABBAS ABU',
|
||||
dob: '19481210',
|
||||
photoHash: '1234567890',
|
||||
phoneNumber: '1234567890',
|
||||
document: 'ID',
|
||||
gender: 'Male',
|
||||
address: '1234567890',
|
||||
user_identifier: '1234567890',
|
||||
current_date: '20250101',
|
||||
majority_age_ASCII: '20',
|
||||
selector_older_than: '1',
|
||||
};
|
||||
|
||||
export const NON_OFAC_DUMMY_INPUT: KycData = {
|
||||
country: 'KEN',
|
||||
idType: 'NATIONAL ID',
|
||||
idNumber: '12345678901234567890123456789012', //32 digits
|
||||
issuanceDate: '20200101',
|
||||
expiryDate: '20290101',
|
||||
fullName: 'John Doe',
|
||||
dob: '19900101',
|
||||
photoHash: '1234567890',
|
||||
phoneNumber: '1234567890',
|
||||
document: 'ID',
|
||||
gender: 'Male',
|
||||
address: '1234567890',
|
||||
user_identifier: '1234567890',
|
||||
current_date: '20250101',
|
||||
majority_age_ASCII: '20',
|
||||
selector_older_than: '1',
|
||||
};
|
||||
|
||||
export const createKycDiscloseSelFromFields = (fieldsToReveal: KycField[]): string[] => {
|
||||
const [lowResult, highResult] = createKycSelector(fieldsToReveal);
|
||||
return [lowResult.toString(), highResult.toString()];
|
||||
};
|
||||
|
||||
export const generateMockKycRegisterInput = async (
|
||||
secretKey?: bigint,
|
||||
ofac?: boolean,
|
||||
secret?: string
|
||||
) => {
|
||||
const kycData = ofac ? OFAC_DUMMY_INPUT : NON_OFAC_DUMMY_INPUT;
|
||||
const serializedData = serializeKycData(kycData).padEnd(KYC_MAX_LENGTH, '\0');
|
||||
|
||||
const msgPadded = Array.from(serializedData, (x) => x.charCodeAt(0));
|
||||
|
||||
const sk = secretKey ? secretKey : BigInt(Math.floor(Math.random() * Number(subOrder - 2n))) + 1n;
|
||||
|
||||
const pk = mulPointEscalar(Base8, sk);
|
||||
console.assert(inCurve(pk), 'Point pk not on curve');
|
||||
console.assert(pk[0] != 0n && pk[1] != 0n, 'pk is zero');
|
||||
|
||||
const [sig, pubKey] = signEdDSA(sk, msgPadded);
|
||||
console.assert(BigInt(sig.S) < subOrder, ' s is greater than scalar field');
|
||||
|
||||
const kycRegisterInput: KycRegisterInput = {
|
||||
data_padded: msgPadded.map((x) => Number(x)),
|
||||
s: BigInt(sig.S),
|
||||
R: sig.R8 as [bigint, bigint],
|
||||
pubKey,
|
||||
secret: secret || '1234',
|
||||
};
|
||||
|
||||
return kycRegisterInput;
|
||||
};
|
||||
|
||||
export const generateCircuitInputsOfac = (data: KycData, smt: SMT, proofLevel: number) => {
|
||||
const name = data.fullName;
|
||||
const dob = data.dob;
|
||||
const yob = data.dob.slice(0, 4);
|
||||
|
||||
const nameDobLeaf = getNameDobLeafKyc(name, dob);
|
||||
const nameYobLeaf = getNameYobLeafKyc(name, yob);
|
||||
|
||||
let root, closestleaf, siblings;
|
||||
if (proofLevel == 2) {
|
||||
({ root, closestleaf, siblings } = generateSMTProof(smt, nameDobLeaf));
|
||||
} else if (proofLevel == 1) {
|
||||
({ root, closestleaf, siblings } = generateSMTProof(smt, nameYobLeaf));
|
||||
} else {
|
||||
throw new Error('Invalid proof level');
|
||||
}
|
||||
|
||||
return {
|
||||
smt_root: formatInput(root),
|
||||
smt_leaf_key: formatInput(closestleaf),
|
||||
smt_siblings: formatInput(siblings),
|
||||
};
|
||||
};
|
||||
|
||||
export const generateKycDiscloseInput = (
|
||||
ofac_input: boolean,
|
||||
nameDobSmt: SMT,
|
||||
nameYobSmt: SMT,
|
||||
identityTree: LeanIMT,
|
||||
ofac: boolean,
|
||||
scope: string,
|
||||
userIdentifier: string,
|
||||
fieldsToReveal?: KycField[],
|
||||
forbiddenCountriesList?: string[],
|
||||
minimumAge?: number,
|
||||
updateTree?: boolean,
|
||||
secret: string = '1234'
|
||||
) => {
|
||||
const data = ofac_input ? OFAC_DUMMY_INPUT : NON_OFAC_DUMMY_INPUT;
|
||||
const serializedData = serializeKycData(data).padEnd(KYC_MAX_LENGTH, '\0');
|
||||
const msgPadded = Array.from(serializedData, (x) => x.charCodeAt(0));
|
||||
const commitment = poseidon2([secret, packBytesAndPoseidon(msgPadded)]);
|
||||
if (updateTree) {
|
||||
identityTree.insert(commitment);
|
||||
}
|
||||
const index = findIndexInTree(identityTree, commitment);
|
||||
const {
|
||||
siblings,
|
||||
path: merkle_path,
|
||||
leaf_depth,
|
||||
} = generateMerkleProof(identityTree, index, COMMITMENT_TREE_DEPTH);
|
||||
|
||||
const nameDobInputs = generateCircuitInputsOfac(data, nameDobSmt, 2);
|
||||
const nameYobInputs = generateCircuitInputsOfac(data, nameYobSmt, 1);
|
||||
|
||||
const fieldsToRevealFinal = fieldsToReveal || [];
|
||||
const compressed_disclose_sel = createKycDiscloseSelFromFields(fieldsToRevealFinal);
|
||||
|
||||
const majorityAgeASCII = minimumAge
|
||||
? minimumAge
|
||||
.toString()
|
||||
.padStart(3, '0')
|
||||
.split('')
|
||||
.map((x) => x.charCodeAt(0))
|
||||
: ['0', '0', '0'].map((x) => x.charCodeAt(0));
|
||||
|
||||
const currentDate = new Date().toISOString().split('T')[0].replace(/-/g, '').split('');
|
||||
|
||||
const circuitInput: KycDiscloseInput = {
|
||||
data_padded: formatInput(msgPadded),
|
||||
compressed_disclose_sel: compressed_disclose_sel,
|
||||
scope: scope,
|
||||
merkle_root: formatInput(BigInt(identityTree.root)),
|
||||
leaf_depth: formatInput(leaf_depth),
|
||||
path: formatInput(merkle_path),
|
||||
siblings: formatInput(siblings),
|
||||
forbidden_countries_list: forbiddenCountriesList || [...Array(120)].map((x) => '0'),
|
||||
ofac_name_dob_smt_leaf_key: nameDobInputs.smt_leaf_key,
|
||||
ofac_name_dob_smt_root: nameDobInputs.smt_root,
|
||||
ofac_name_dob_smt_siblings: nameDobInputs.smt_siblings,
|
||||
ofac_name_yob_smt_leaf_key: nameYobInputs.smt_leaf_key,
|
||||
ofac_name_yob_smt_root: nameYobInputs.smt_root,
|
||||
ofac_name_yob_smt_siblings: nameYobInputs.smt_siblings,
|
||||
selector_ofac: ofac ? ['1'] : ['0'],
|
||||
user_identifier: userIdentifier,
|
||||
current_date: currentDate,
|
||||
majority_age_ASCII: majorityAgeASCII,
|
||||
secret: secret,
|
||||
};
|
||||
|
||||
return circuitInput;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user