Merge pull request #1620 from selfxyz/release/staging-2026-01-16

Release to Staging - 2026-01-16
This commit is contained in:
Justin Hernandez
2026-01-21 16:09:30 -08:00
committed by GitHub
165 changed files with 19097 additions and 3154 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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"
}

View File

@@ -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"

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;

View File

@@ -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,
},
});

View 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} />;
};

View File

@@ -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;

View File

@@ -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 (
<>

View File

@@ -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';

View File

@@ -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);
}, []);

View File

@@ -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 = {

View File

@@ -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,

View File

@@ -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>

View File

@@ -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';

View File

@@ -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}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 &quot;Skip when only one
document&quot; 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 };

View File

@@ -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':

View File

@@ -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 = () => {

View File

@@ -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"

View File

@@ -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.',

View File

@@ -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 => (

View File

@@ -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>
</>

View File

@@ -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

View File

@@ -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}

View File

@@ -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}

View File

@@ -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,
}}
>

View File

@@ -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);

View File

@@ -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}

View File

@@ -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(

View File

@@ -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>
);
};

View File

@@ -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)}`,
);
};

View 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.',
);
}
};

View File

@@ -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,

View 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;
};

View File

@@ -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())
);
}

View File

@@ -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}

View File

@@ -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);

View File

@@ -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"
}
}

View 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;
}

View 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);

File diff suppressed because one or more lines are too long

View File

@@ -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);

View File

@@ -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;

View File

@@ -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(),

View File

@@ -0,0 +1,4 @@
pragma circom 2.1.9;
include "../register_kyc.circom";
component main = REGISTER_KYC();

View 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;
}

View File

@@ -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];
}
}
}

View 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;
}

View File

@@ -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;

View 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];
}

View 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;
}

View File

@@ -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; // ','

View 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;
}

View 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();
}

View 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;
}

View 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;
}

View 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;
}

View 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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View 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];
// }
// }

View File

@@ -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",

View File

@@ -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[@]}"

View 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}"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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 {

View 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);
});
});

View 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');
}
});
});

View 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;
}
});
});

View 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');
});
});

View File

@@ -0,0 +1,5 @@
pragma circom 2.1.9;
include "../../../../circuits/utils/kyc/date/isOlderThan.circom";
component main = IsOlderThan();

View File

@@ -0,0 +1,3 @@
include "../../../../circuits/utils/kyc/date/isValid.circom";
component main = IsValidFullYear();

View 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');
});
});

View File

@@ -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);

View File

@@ -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);

View File

@@ -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';

View File

@@ -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",

View File

@@ -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';

View File

@@ -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,

View File

@@ -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...');

View File

@@ -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';

View 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];
}

View 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;
}

View 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;
}

View 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