Files
self/sdk/sdk-go/utils.go
Nesopie e77247f372 Feat/kyc (#1623)
* feat: selfrica circuit and tests

* chore: remove unused code

* feat: test for ofac,date and olderthan

* fix: public signal constant

* feat: add contract tests

* feat: helper function to gen TEE input

* feat: gen circuit inputs with signature

* feat: seralized base64

* fix: DateIsLessFullYear componenet

* feat: register circuit for selfrica

* feat: selfrica disclose circuit and test

* fix: common module error

* feat: add more test and fix constant

* fix: commitment calculation

* feat: selfrica contracts

* test: selfrica register using unified circuit

* feat: register persona and selfrica circuit

* feat: selfrica circuit and tests

* chore: remove unused code

* feat: test for ofac,date and olderthan

* fix: public signal constant

* feat: add contract tests

* feat: helper function to gen TEE input

* feat: gen circuit inputs with signature

* feat: seralized base64

* fix: DateIsLessFullYear componenet

* feat: register circuit for selfrica

* feat: selfrica disclose circuit and test

* fix: common module error

* feat: add more test and fix constant

* fix: commitment calculation

* feat: selfrica contracts

* test: selfrica register using unified circuit

* feat: register persona and selfrica circuit

* refactor: contract size reduction for IdentityVerificationHubImplV2

export function logic to external libs, reduce compiler runs to 200, update deploy scripts to link new libs

* feat: disclose circuit for persona

* feat: update  persona ofac trees

* feat; register circuit for selfper

* feat: disclose test for selfper

* chore: refactor

* chore : remove unused circuits

* chore: rename selfper to kyc

* chore: update comments

* feat: constrain s to be 251 bit

* feat: add range check on majority ASCII and comments

* feat: range check on neg_r_inv

* chore: remove is pk zero constrain

* merge dev

* feat: add registerPubkey function to Selfrica with GCPJWT Verification

* test: add testing for GCPJWT verification on Selfrica

* fix: script that calls register_selfrica circuits (ptau:14 -> ptau:15)

* fix: get remaining Selfrica tests working with proper import paths

* refactor: store pubkeys as string

also add some comment code for registerPubkey function

* refactor: remove registerPubkeyCommitment function

some tests now skipped as awaiting changes to how pubkeys are stored (string instead of uint256)

* feat: use hex decoding for the pubkey commitment

* test: adjust tests for pubkey being string again

* fix: remove old references to registerPubkey

* docs: add full natspec for IdentityRegistrySelfricaImplV1

* docs: update files in rest of the repo for Selfrica attestation type

* test: fix broken tests

* fix: builds and move to kyc from selfrica

* fix: constrain r_inv, Rx, s, T

* feat: eddsa

* feat: add onlyTEE check to registerPubkeyCommitment

onlyOwner is able to change onlyTEE

* refactor: update gcpRootCAPubkeyHash to be changeable by owner

* feat: add events for update functions

* style: move functions to be near other similar functions

* fix: kyc happy flow

* fix: all contract tests passing

| fix: timestamp conversion with Date(), migrate to V2 for endToEnd test, scope formatting, fix register aadhaar issue by using block.timestamp instead of Date.now(), fix changed getter function name, enable MockGCPJWTVerifier with updated file paths, add missing LeanIMT import, fix user identifier format

* audit: bind key offset-value offset and ensure image_digest only occurs once in the payload

* fix: constrain bracket

* chore: update comment

* audit: hardcode attestation id

* audit: make sure R and pubkey are on the curve

* audit: ensure pubkey is within bounds

* fix: all contract tests passing

* feat: change max length to 99 from 74

* audit: don't check sha256 padding

* audit: check the last window as well

* audit: single occurance for eat_nonce and image_digest

* audit: check if the certs are expired

* audit: add the timestamp check to the contract

* audit: make sure the person is less than 255 years of age

* audit fixes

* chore: yarn.lock

* fix: build fixes

* fix: aadhaar timestamp

* lint

* fix: types

* format

---------

Co-authored-by: vishal <vishalkoolkarni0045@gmail.com>
Co-authored-by: Evi Nova <tranquil_flow@protonmail.com>
2026-01-19 15:54:37 +05:30

532 lines
17 KiB
Go

package self
import (
"crypto/sha256"
"fmt"
"math"
"math/big"
"regexp"
"strings"
"github.com/selfxyz/self/sdk/sdk-go/common"
"golang.org/x/crypto/ripemd160"
)
// Constants for attestation types
const (
Passport AttestationId = 1
EUCard AttestationId = 2
Aadhaar AttestationId = 3
SelfricaIdCard AttestationId = 4
)
// DiscloseIndicesEntry defines the indices for different data fields in the public signals
type DiscloseIndicesEntry struct {
RevealedDataPackedIndex int
ForbiddenCountriesListPackedIndex int
NullifierIndex int
AttestationIdIndex int
MerkleRootIndex int
CurrentDateIndex int
NamedobSmtRootIndex int
NameyobSmtRootIndex int
ScopeIndex int
UserIdentifierIndex int
PassportNoSmtRootIndex int
}
// DiscloseIndices maps attestation IDs to their respective index configurations
var DiscloseIndices = map[AttestationId]DiscloseIndicesEntry{
Passport: {
RevealedDataPackedIndex: 0,
ForbiddenCountriesListPackedIndex: 3,
NullifierIndex: 7,
AttestationIdIndex: 8,
MerkleRootIndex: 9,
CurrentDateIndex: 10,
NamedobSmtRootIndex: 17,
NameyobSmtRootIndex: 18,
ScopeIndex: 19,
UserIdentifierIndex: 20,
PassportNoSmtRootIndex: 16,
},
EUCard: {
RevealedDataPackedIndex: 0,
ForbiddenCountriesListPackedIndex: 4,
NullifierIndex: 8,
AttestationIdIndex: 9,
MerkleRootIndex: 10,
CurrentDateIndex: 11,
NamedobSmtRootIndex: 17,
NameyobSmtRootIndex: 18,
ScopeIndex: 19,
UserIdentifierIndex: 20,
PassportNoSmtRootIndex: 99,
},
Aadhaar: {
RevealedDataPackedIndex: 2,
ForbiddenCountriesListPackedIndex: 6,
NullifierIndex: 0,
AttestationIdIndex: 10,
MerkleRootIndex: 16,
CurrentDateIndex: 11,
NamedobSmtRootIndex: 14,
NameyobSmtRootIndex: 15,
ScopeIndex: 17,
UserIdentifierIndex: 18,
PassportNoSmtRootIndex: 99,
},
// Selfrica ID Card - see CircuitConstantsV2.sol for layout documentation
SelfricaIdCard: {
RevealedDataPackedIndex: 0,
ForbiddenCountriesListPackedIndex: 9,
NullifierIndex: 13,
AttestationIdIndex: 29,
MerkleRootIndex: 17,
CurrentDateIndex: 21,
NamedobSmtRootIndex: 18,
NameyobSmtRootIndex: 19,
ScopeIndex: 16,
UserIdentifierIndex: 20,
PassportNoSmtRootIndex: 99,
},
}
// Field names for revealed data
const (
IssuingState string = "issuingState"
Name string = "name"
IdNumber string = "idNumber"
Nationality string = "nationality"
DateOfBirth string = "dateOfBirth"
Gender string = "gender"
ExpiryDate string = "expiryDate"
OlderThan string = "olderThan"
Ofac string = "ofac"
)
// RevealedDataIndicesEntry defines the start and end indices for different data fields
type RevealedDataIndicesEntry struct {
IssuingStateStart int
IssuingStateEnd int
NameStart int
NameEnd int
IdNumberStart int
IdNumberEnd int
NationalityStart int
NationalityEnd int
DateOfBirthStart int
DateOfBirthEnd int
GenderStart int
GenderEnd int
ExpiryDateStart int
ExpiryDateEnd int
OlderThanStart int
OlderThanEnd int
OfacStart int
OfacEnd int
}
// RevealedDataIndices maps attestation IDs to their data field indices
var RevealedDataIndices = map[AttestationId]RevealedDataIndicesEntry{
Passport: {
IssuingStateStart: 2,
IssuingStateEnd: 4,
NameStart: 5,
NameEnd: 43,
IdNumberStart: 44,
IdNumberEnd: 52,
NationalityStart: 54,
NationalityEnd: 56,
DateOfBirthStart: 57,
DateOfBirthEnd: 62,
GenderStart: 64,
GenderEnd: 64,
ExpiryDateStart: 65,
ExpiryDateEnd: 70,
OlderThanStart: 88,
OlderThanEnd: 89,
OfacStart: 90,
OfacEnd: 92,
},
EUCard: {
IssuingStateStart: 2,
IssuingStateEnd: 4,
NameStart: 60,
NameEnd: 89,
IdNumberStart: 5,
IdNumberEnd: 13,
NationalityStart: 45,
NationalityEnd: 47,
DateOfBirthStart: 30,
DateOfBirthEnd: 35,
GenderStart: 37,
GenderEnd: 37,
ExpiryDateStart: 38,
ExpiryDateEnd: 43,
OlderThanStart: 90,
OlderThanEnd: 91,
OfacStart: 92,
OfacEnd: 93,
},
Aadhaar: {
IssuingStateStart: 81,
IssuingStateEnd: 111,
NameStart: 9,
NameEnd: 70,
IdNumberStart: 71,
IdNumberEnd: 74,
NationalityStart: 999,
NationalityEnd: 999,
DateOfBirthStart: 1,
DateOfBirthEnd: 8,
GenderStart: 0,
GenderEnd: 0,
ExpiryDateStart: 999,
ExpiryDateEnd: 999,
OlderThanStart: 118,
OlderThanEnd: 118,
OfacStart: 116,
OfacEnd: 117,
},
}
// AllIds contains all valid attestation IDs
var AllIds = map[AttestationId]bool{
Passport: true,
EUCard: true,
Aadhaar: true,
SelfricaIdCard: true,
}
// BytesCount maps attestation IDs to their respective byte counts
var BytesCount = map[AttestationId][]int{
Passport: {31, 31, 31},
EUCard: {31, 31, 31, 1},
Aadhaar: {31, 31, 31, 26},
SelfricaIdCard: {31, 31, 31, 31, 31, 31, 31, 31, 31},
}
// trimU0000 filters out null characters (\u0000) from a slice of strings
func trimU0000(unpackedReveal []string) []string {
var result []string
for _, value := range unpackedReveal {
if value != "\u0000" {
result = append(result, value)
}
}
return result
}
// UnpackForbiddenCountriesList unpacks a list of packed forbidden country codes into an array of 3-character country codes.
//
// Parameters:
// - forbiddenCountriesListPacked: A slice of packed strings representing forbidden countries
//
// Returns:
// - A slice of 3-character country codes extracted from the packed input
func UnpackForbiddenCountriesList(forbiddenCountriesListPacked []string) []string {
// Unpack the revealed data using the unpackReveal function
unpacked := common.UnpackReveal(forbiddenCountriesListPacked, "id")
trimmed := trimU0000(unpacked)
var countries []string
// Join all trimmed strings to work with characters
joined := strings.Join(trimmed, "")
// Extract 3-character country codes
for i := 0; i < len(joined); i += 3 {
if i+3 <= len(joined) {
countryCode := joined[i : i+3]
if len(countryCode) == 3 {
countries = append(countries, countryCode)
}
}
}
return countries
}
// CastToUserIdentifier converts a big integer to user identifier string based on the specified type
func CastToUserIdentifier(bigInt *big.Int, userIdType UserIDType) string {
switch userIdType {
case UserIDTypeHex:
return CastToAddress(bigInt)
case UserIDTypeUUID:
return CastToUUID(bigInt)
default:
return bigInt.String()
}
}
// CastToAddress converts big integer to hex address format (0x + 40 hex chars)
func CastToAddress(bigInt *big.Int) string {
hexStr := bigInt.Text(16) // Convert to hex without 0x prefix
// Pad to 40 characters (20 bytes = 40 hex chars)
if len(hexStr) < 40 {
hexStr = fmt.Sprintf("%040s", hexStr)
}
return "0x" + hexStr
}
// CastToUUID converts big integer to UUID format
func CastToUUID(bigInt *big.Int) string {
hexStr := bigInt.Text(16) // Convert to hex without 0x prefix
// Pad to 32 characters
if len(hexStr) < 32 {
hexStr = fmt.Sprintf("%032s", hexStr)
}
// Format as UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
return fmt.Sprintf("%s-%s-%s-%s-%s",
hexStr[0:8], hexStr[8:12], hexStr[12:16], hexStr[16:20], hexStr[20:32])
}
// CalculateUserIdentifierHash generates a deterministic user identifier hash from the provided context data.
//
// The function computes a SHA-256 hash of the input buffer, then applies a RIPEMD-160 hash to the result.
// The final output is a hexadecimal string, left-padded with zeros to 40 characters and prefixed with "0x".
//
// Parameters:
// - userContextData: The byte slice containing user context data to hash
//
// Returns:
// - A 40-character hexadecimal user identifier string prefixed with "0x"
func CalculateUserIdentifierHash(userContextData []byte) string {
// Compute SHA-256 hash
sha256Hasher := sha256.New()
sha256Hasher.Write(userContextData)
sha256Hash := sha256Hasher.Sum(nil)
// Compute RIPEMD-160 hash of the SHA-256 hash
ripemdHasher := ripemd160.New()
ripemdHasher.Write(sha256Hash)
ripemdHash := ripemdHasher.Sum(nil)
hexString := fmt.Sprintf("%x", ripemdHash)
// Pad with leading zeros to ensure 40 hex chars
if len(hexString) < 40 {
hexString = fmt.Sprintf("%040s", hexString)
}
return "0x" + hexString
}
// PublicSignals represents an array of numeric strings, equivalent to snarkjs PublicSignals
type PublicSignals []string
// GetRevealedDataPublicSignalsLength returns the number of public signals containing
// revealed data for the specified attestation ID.
//
// Returns an error if the attestation ID is not supported.
//
// Parameters:
// - attestationId: The attestation ID for which to determine the number of revealed data public signals
//
// Returns:
// - The number of public signals corresponding to revealed data
// - An error if the attestation ID is invalid
func GetRevealedDataPublicSignalsLength(attestationId AttestationId) (int, error) {
switch attestationId {
case Passport:
return int(93 / 31), nil
case EUCard:
return int(math.Ceil(94.0 / 31.0)), nil
case Aadhaar:
return int(math.Ceil(119.0 / 31.0)), nil
case SelfricaIdCard:
return 9, nil
default:
return 0, fmt.Errorf("invalid attestation ID: %d", attestationId)
}
}
// GetRevealedDataBytes extracts and returns the revealed data bytes from the public signals
// for a given attestation ID.
//
// Iterates over the relevant public signals, unpacks each into its constituent bytes according
// to the attestation's byte structure, and accumulates all revealed bytes into a single array.
//
// Parameters:
// - attestationId: The attestation ID specifying the format of revealed data
// - publicSignals: The array of public signals containing packed revealed data
//
// Returns:
// - An array of bytes representing the revealed data for the specified attestation
// - An error if the attestation ID is invalid or if there's an issue processing the signals
func GetRevealedDataBytes(attestationId AttestationId, publicSignals PublicSignals) ([]int, error) {
// Get the length of revealed data public signals
length, err := GetRevealedDataPublicSignalsLength(attestationId)
if err != nil {
return nil, err
}
// Get the disclose indices for this attestation ID
discloseIndices, exists := DiscloseIndices[attestationId]
if !exists {
return nil, fmt.Errorf("disclose indices not found for attestation ID: %d", attestationId)
}
// Get the bytes count for this attestation ID
bytesCount, exists := BytesCount[attestationId]
if !exists {
return nil, fmt.Errorf("bytes count not found for attestation ID: %d", attestationId)
}
var bytes []int
for i := 0; i < length; i++ {
signalIndex := discloseIndices.RevealedDataPackedIndex + i
publicSignal := new(big.Int)
publicSignal, success := publicSignal.SetString(publicSignals[signalIndex], 10)
if !success {
return nil, fmt.Errorf("failed to parse public signal at index %d: %s", signalIndex, publicSignals[signalIndex])
}
// Extract bytes from the public signal
for j := 0; j < bytesCount[i]; j++ {
// Extract the least significant byte (equivalent to publicSignal & 0xffn)
byteVal := new(big.Int)
byteVal.And(publicSignal, big.NewInt(0xff))
bytes = append(bytes, int(byteVal.Int64()))
publicSignal.Rsh(publicSignal, 8)
}
}
return bytes, nil
}
// FormatRevealedDataPacked extracts and formats revealed data from public signals
func FormatRevealedDataPacked(attestationID AttestationId, publicSignals PublicSignals) (GenericDiscloseOutput, error) {
revealedDataPacked, err := GetRevealedDataBytes(attestationID, publicSignals)
if err != nil {
return GenericDiscloseOutput{}, err
}
discloseIndices, exists := DiscloseIndices[attestationID]
if !exists {
return GenericDiscloseOutput{}, fmt.Errorf("disclose indices not found for attestation ID: %d", attestationID)
}
// Convert revealedDataPacked ([]int) to byte array for string operations
revealedDataPackedBytes := make([]byte, len(revealedDataPacked))
for i, b := range revealedDataPacked {
revealedDataPackedBytes[i] = byte(b)
}
// Get revealed data indices for this attestation ID
revealedDataIndices, exists := RevealedDataIndices[attestationID]
if !exists {
return GenericDiscloseOutput{}, fmt.Errorf("revealed data indices not found for attestation ID: %d", attestationID)
}
// Extract nullifier
nullifier := publicSignals[discloseIndices.NullifierIndex]
// Extract forbidden countries list packed
fcStartIndex := discloseIndices.ForbiddenCountriesListPackedIndex
forbiddenCountriesListPacked := publicSignals[fcStartIndex : fcStartIndex+4]
// Extract issuing state
issuingState := string(revealedDataPackedBytes[revealedDataIndices.IssuingStateStart : revealedDataIndices.IssuingStateEnd+1])
// Extract name with cleaning (equivalent to regex replacements and trim)
nameRaw := string(revealedDataPackedBytes[revealedDataIndices.NameStart : revealedDataIndices.NameEnd+1])
name := cleanName(nameRaw)
// Extract ID number
idNumber := string(revealedDataPackedBytes[revealedDataIndices.IdNumberStart : revealedDataIndices.IdNumberEnd+1])
// Extract nationality
nationality := ""
if attestationID == Aadhaar {
nationality = "IND"
} else {
nationality = string(revealedDataPackedBytes[revealedDataIndices.NationalityStart : revealedDataIndices.NationalityEnd+1])
}
// Extract date of birth
var dateOfBirth string
if attestationID == Aadhaar {
dobBytes := revealedDataPackedBytes[revealedDataIndices.DateOfBirthStart : revealedDataIndices.DateOfBirthEnd+1]
var dobStrings []string
for _, b := range dobBytes {
dobStrings = append(dobStrings, fmt.Sprintf("%d", int(b)))
}
dateOfBirth = strings.Join(dobStrings, "")
} else {
dateOfBirth = string(revealedDataPackedBytes[revealedDataIndices.DateOfBirthStart : revealedDataIndices.DateOfBirthEnd+1])
}
// Extract gender
gender := string(revealedDataPackedBytes[revealedDataIndices.GenderStart : revealedDataIndices.GenderEnd+1])
// Extract expiry date
var expiryDate string
if attestationID == Aadhaar {
expiryDate = "UNAVAILABLE"
} else {
expiryDate = string(revealedDataPackedBytes[revealedDataIndices.ExpiryDateStart : revealedDataIndices.ExpiryDateEnd+1])
}
// Extract minimum age (olderThan)
var minimumAge string
if attestationID == Aadhaar {
firstByte := revealedDataPackedBytes[revealedDataIndices.OlderThanStart]
minimumAge = fmt.Sprintf("%02d", int(firstByte))
} else {
minimumAge = string(revealedDataPackedBytes[revealedDataIndices.OlderThanStart : revealedDataIndices.OlderThanEnd+1])
}
// Extract OFAC data and convert to boolean array
ofacBytes := revealedDataPackedBytes[revealedDataIndices.OfacStart : revealedDataIndices.OfacEnd+1]
ofac := make([]bool, len(ofacBytes))
for i, b := range ofacBytes {
ofac[i] = !(b != 0)
}
if len(ofac) < 3 {
ofac = append([]bool{false}, ofac...)
}
// Return the structured output
return GenericDiscloseOutput{
Nullifier: nullifier,
ForbiddenCountriesListPacked: forbiddenCountriesListPacked,
IssuingState: removeNullBytes(issuingState),
Name: removeNullBytes(name),
IdNumber: idNumber,
Nationality: nationality,
DateOfBirth: dateOfBirth,
Gender: gender,
ExpiryDate: expiryDate,
MinimumAge: minimumAge,
Ofac: ofac,
}, nil
}
// removeNullBytes removes null bytes (\x00) from a string
func removeNullBytes(str string) string {
return strings.ReplaceAll(str, "\x00", "")
}
// cleanName cleans the name string equivalent to the TypeScript regex operations
// .replace(/([A-Z])<+([A-Z])/g, '$1 $2').replace(/</g, ").trim()
func cleanName(nameRaw string) string {
// Replace pattern ([A-Z])<+([A-Z]) with '$1 $2'
re1 := regexp.MustCompile(`([A-Z])<+([A-Z])`)
name := re1.ReplaceAllString(nameRaw, "$1 $2")
// Replace all remaining '<' characters
name = strings.ReplaceAll(name, "<", "")
// Trim whitespace
name = strings.TrimSpace(name)
return name
}