Skip to content

Instantly share code, notes, and snippets.

@tripolskypetr
Created May 10, 2026 13:34
Show Gist options
  • Select an option

  • Save tripolskypetr/6c86ab4a00bf189846fce6ff50849ce3 to your computer and use it in GitHub Desktop.

Select an option

Save tripolskypetr/6c86ab4a00bf189846fce6ff50849ce3 to your computer and use it in GitHub Desktop.
SIGNING.md

SDK maqsadli arxitekturasi

TODO

TadSigningSDK faqat bitta metodni taqdim etadi — sign(dto). Bank ilovasining dasturchisi bitta qator yozadi:

let result = await TadSigning.sign(dto: ["amount": 1000, "to": "IBAN..."])

Va javob sifatida statusOk yoki statusError(error:, errorCode:) oladi. Qolgan barchasi — SDK ichida.


Roadmap: Flutter ulashi

Keyinchalik SDK MethodChannel / FFI orqali Flutter o'ramini oladi — bu nativ iOS/Android SDK modullariga chaqiruvlarni yo'naltiradigan ingichka abstraksiya qatlami. Dart'da SDK alohida amalga oshirilmaydi. Dart kod dekompilyatsiya qilinadi va VM ichida ishlaydi, shuning uchun barcha kriptografiya, biometriya, saqlash va tarmoq chaqiruvlari nativ tomonda qoladi.

iOS SDK'ning ommaviy xulq-atvoriga talablar:

  • errorCode — bu enum kabi satr, uning nomlari iOS va Android'da bir xilda tabiiy yangraydi. WEBAUTHN_USER_NOT_FOUND, INTEGRITY_TAMPERED_OS, JWT_SIGNATURE_INVALID — neytral. KEYCHAIN_FAILURE, SECURE_ENCLAVE_UNAVAILABLE, KEYSTORE_LOCKED, STRONGBOX_UNAVAILABLE, INTEGRITY_JAILBREAK (faqat iOS atamasi), INTEGRITY_ROOT (faqat Android atamasi) — platformaga bog'liq, tashqariga chiqmasligi kerak. Ichida iOS SDK istalgan tekshiruvlarni va istalgan platforma API'larini ishlatishi mumkin; ommaviy errorCode'da bularning barchasi umumiy nomlar to'plamiga normallashtiriladi.
  • Barcha WEBAUTHN xatolari backend'dan keladi. SDK ularni o'ylab topmaydi — faqat satr sifatida tashqariga uzatadi va WEBAUTHN_ prefiksi bo'yicha registratsiyani wipe qilish haqida qaror qabul qiladi.
  • Saqlash — har bir platformaning umumiy interfeys ortidagi nativ mexanizmi. iOS'da — Keychain. Android'da — EncryptedSharedPreferences. Flutter'da hech qanday yangilik qo'shilmaydi: MethodChannel chaqiruvni platformaga uzatadi va o'sha Keychain yoki o'sha EncryptedSharedPreferences ishlaydi. Hech qanday flutter_secure_storage yoki boshqa "kross-platforma" yechimi yo'q. Yagona talab — iOS SDK ichida Keychain'ni ingichka SecureStorage interfeysi ortiga olib o'tish, shunda kuzgu Android SDK'da xuddi shunday kontrakt bo'ladi.
  • Har bir ommaviy API metodida kuzgu Kotlin ekvivalenti bo'lishi shart, aks holda MethodChannel uni uzatolmaydi. Metod nomlari va enum kodlari yagona.

Ommaviy errorCode'ga platformaga xos nom qo'shadigan yoki iOS/Android API kuzgu mosligini buzadigan har qanday PR rad etilishi kerak.


Ommaviy API

Konfiguratsiya (ilova ishga tushirilganda bir marta)

TadSigning.configure(
    apiBaseUrl:    URL(string: "https://signing.tadi.uz")!,
    publicKeyPem:  "-----BEGIN PUBLIC KEY-----\n...",
    rpId:          "signing.tadi.uz",
    serviceName:   "tad-signing-demo",
    bankId:        "demo-user-001"    // shu yerda qotirilgan, sign()'ga uzatilmaydi
)

DTO'ni imzolash

public enum TadSigning {

    /// SDK konfiguratsiyasi. Ilova ishga tushirilganda bir marta chaqiriladi.
    public static func configure(
        apiBaseUrl:   URL,
        publicKeyPem: String,
        rpId:         String,
        serviceName:  String,
        bankId:       String
    )

    /// Registratsiya tekshiruvi. Lokal: agar himoyalangan saqlashda joriy
    /// bankId uchun (deviceId, credentialId) jufti mavjud bo'lsa true qaytaradi.
    /// Tarmoq so'rovlari yo'q.
    public static func isRegistered() -> Bool

    /// Lokal saqlashdan registratsiyani o'chiradi.
    /// deviceId — bu SDK tomonidan registratsiya paytida chiqarilgan tasodifiy UUID;
    /// u qurilmaga BOG'LANMAGAN va qayta ishlatilmaydi. logout()'dan keyingi
    /// birinchi sign() yangi tasodifiy deviceId bilan to'liq registratsiyani bajaradi.
    /// Tarmoq so'rovlari yo'q; serverdagi credential o'zi muddati tugaydi.
    public static func logout()

    /// isRegistered() bayrog'i bo'yicha BITTA operatsiyani bajaradi:
    /// - agar isRegistered() == false — registratsiya bajariladi. Bitta /api/v1/auth/register/* chaqiruvi.
    /// - agar isRegistered() == true  — verifikatsiya bajariladi. Bitta /api/v1/auth/verify chaqiruvi.
    /// Har bir sign() chaqiruvida aniq BITTA operatsiya, ikkalasi emas.
    ///
    /// errorCode WEBAUTHN_, JWT_ yoki INTEGRITY_ prefiksi bilan bo'lsa — SDK lokal
    /// registratsiyani tozalaydi va statusError qaytaradi. Chaqiruvchi sign(dto)'ni
    /// qaytadan chaqirishi mumkin — keyingi chaqiruv registratsiyadan boshlanadi.
    public static func sign(dto: [String: Any]) async -> TadSigningStatus
}

public enum TadSigningStatus {
    case statusOk
    case statusError(error: String, errorCode: String)
}

statusError maydonlari

Maydon Mazmuni
error Loglar / foydalanuvchi dialogi uchun odam o'qiydigan xabar
errorCode Satr. To'rt sinfdan biri — quyidagi jadvalga qarang

JWT, deviceId, credentialId, verificationId, registrationIdchaqiruvchiga qaytarilmaydi.

errorCode sinflari

Sinf / prefiks Manba SDK nima qiladi
WEBAUTHN_* (masalan, WEBAUTHN_USER_NOT_FOUND, WEBAUTHN_FACE_EXPIRED, WEBAUTHN_REPLAY_USED, WEBAUTHN_EMBEDDING_MISMATCH, WEBAUTHN_SIGN_COUNT, WEBAUTHN_REGISTRATION_ID_MISMATCH, WEBAUTHN_CHALLENGE_EXPIRED, WEBAUTHN_DTO_EXPIRED) Server Registratsiyani wipe qiladi, statusError qaytaradi. Auto-retry yo'q. Keyingi sign() registratsiyadan boshlanadi.
JWT_* (masalan, JWT_SIGNATURE_INVALID, JWT_EXPIRED, JWT_DEVICE_MISMATCH, JWT_BANK_MISMATCH, JWT_FIELD_MISMATCH, JWT_ALG_MISMATCH) SDK ichida public.pem va HOWTO.md bo'yicha lokal JWT validatsiya Registratsiyani wipe qiladi, statusError qaytaradi. Imzo buzilgan — davom ettirib bo'lmaydi.
INTEGRITY_* (masalan, INTEGRITY_TAMPERED_OS, INTEGRITY_DEBUGGER, INTEGRITY_PROXY, INTEGRITY_HOOKING, INTEGRITY_EMULATOR, INTEGRITY_PINNING) Lokal yaxlitlik tekshiruvlari Registratsiyani wipe qiladi, statusError qaytaradi. Ichida SDK istalgan tekshiruvlarni amalga oshirishi mumkin; tashqariga faqat shu neytral satrlar chiqadi. INTEGRITY_TAMPERED_OS jailbreak (iOS) va root (Android)'ni qoplaydi — ikkala platforma uchun yagona nom.
FACEIDS_*, USER_CANCELLED, CAMERA_UNAVAILABLE, NETWORK Server / foydalanuvchi / platforma wipe'siz statusError. Registratsiya yaroqli qoladi, keyingi sign() qisqa "faqat verifikatsiya" yo'lidan o'tadi.

Ommaviy errorCode'da hech qanday platformaga xos nomlar (SECURE_ENCLAVE_*, KEYCHAIN_*, KEYSTORE_*, STRONGBOX_*, JAILBREAK_*, ROOT_*, INTEGRITY_ prefiksisiz PROXY_*) bo'lmasligi kerak. Har bir nom iOS va Android'da bir xil tabiiy o'qilishi shart — bu Flutter o'ramining talabi, u ikkala platformadan bir xil qiymatlarni proksilaydi.


sign(dto) hayot sikli

flowchart TD
  Start([App: TadSigning.sign(dto)])
  Start --> Integrity{Integrity checks<br/>pass?}
  Integrity -- no --> ErrInt([statusError<br/>INTEGRITY_xxx])
  Integrity -- yes --> Lock[bankId bo'yicha<br/>mutex olish]
  Lock --> Read{isRegistered?<br/>saqlashda<br/>deviceId + credentialId bormi}

  Read -- no --> Reg[Subflow: registratsiya<br/>bitta /api/v1/auth/register/* chaqiruvi]
  Read -- yes --> Ver[Subflow: verifikatsiya<br/>bitta /api/v1/auth/verify chaqiruvi]

  Reg -- ok --> Done([statusOk<br/>registratsiya yozildi])
  Reg -- error --> WipeIfDirty[Agar yozuv qisman<br/>yaratilgan bo'lsa — wipe]
  WipeIfDirty --> ErrReg([statusError<br/>aniq kod])

  Ver -- ok --> Done
  Ver -- WEBAUTHN_xxx --> Wipe[saqlashdan<br/>registratsiyani wipe qilish]
  Ver -- JWT_xxx --> Wipe
  Ver -- INTEGRITY_xxx --> Wipe
  Ver -- other error --> ErrOther([statusError<br/>aniq kod<br/>registratsiya butun])

  Wipe --> ErrWipe([statusError<br/>aniq kod<br/>keyingi sign&#40;&#41;<br/>registratsiyadan boshlanadi])

  Done --> Unlock[mutex'ni qaytarish]
  ErrInt --> Unlock
  ErrReg --> Unlock
  ErrOther --> Unlock
  ErrWipe --> Unlock

  classDef chk fill:#FFF4E5,stroke:#A86200,color:#5C3700
  classDef sub fill:#E6EEF7,stroke:#1F3A5F,color:#1F3A5F
  classDef ok  fill:#EAF3DE,stroke:#3B6D11,color:#27500A,font-weight:bold
  classDef err fill:#FCEBEB,stroke:#A32D2D,color:#501313,font-weight:bold
  classDef rec fill:#FFE8CC,stroke:#A86200,color:#5C3700

  class Integrity,Read chk
  class Reg,Ver,Lock,Unlock,WipeIfDirty sub
  class Done ok
  class ErrInt,ErrReg,ErrOther,ErrWipe err
  class Wipe rec
Loading

Asosiy qoidalar:

  • sign() har bir chaqiruvda bitta operatsiya bajaradi: yo registratsiya, yo verifikatsiya. Ikkalasi emas. Qaror isRegistered() bo'yicha qabul qilinadi.
  • Agar isRegistered() == falsesign() registratsiyani bajaradi. Muvaffaqiyatdan keyin saqlashda deviceId + credentialId jufti paydo bo'ladi va sign() statusOk qaytaradi. Bu chaqiruvda DTO imzolanmagan — imzolash uchun yana bitta sign() chaqiruvi kerak.
  • Agar isRegistered() == truesign() uzatilgan dto bilan verifikatsiyani bajaradi. Muvaffaqiyatdan keyin statusOk, registratsiya saqlanadi.
  • Agar verify WEBAUTHN_* / JWT_* / INTEGRITY_* bilan tushsa — wipe + statusError. Keyingi sign() registratsiyadan boshlanadi (chunki isRegistered() endi false).
  • Boshqa barcha xatolar (USER_CANCELLED, CAMERA_UNAVAILABLE, NETWORK, FACEIDS_*) — wipe'siz statusError. Registratsiya yaroqli qoladi.
  • Bitta sign() chaqiruvi ichida auto-retry yo'q.

Hujjatni imzolash uchun chaqiruvchi koddagi tipik ketma-ketlik:

if !TadSigning.isRegistered() {
    // birinchi sign() registratsiyani bajaradi; bo'sh dto bo'lishi mumkin
    let s1 = await TadSigning.sign(dto: [:])
    guard case .statusOk = s1 else { return /* xatoni qayta ishlash */ }
}
// registratsiyadan keyin — haqiqiy DTO bilan sign() imzolashni bajaradi
let s2 = await TadSigning.sign(dto: realDto)

Agar registratsiya allaqachon mavjud bo'lsa, birinchi if o'tkazib yuboriladi va imzolash bitta chaqiruvda amalga oshiriladi.


Registratsiya pastsikli

flowchart TD
  Start([Subflow: registratsiya])
  Start --> Gen[deviceId = UUID&#40;&#41;<br/>HAR registratsiya uchun tasodifiy,<br/>qurilmaga bog'lanmagan]
  Gen --> Cam[Kamera + liveness, 2 kadr]
  Cam -- error --> ErrCam([fail: USER_CANCELLED /<br/>CAMERA_UNAVAILABLE])
  Cam -- ok --> Cap[POST /api/v1/check/capture]
  Cap --> CapV{public.pem bo'yicha<br/>JWT verifikatsiyasi<br/>HOWTO.md}
  CapV -- error --> ErrJwt([fail: JWT_xxx])
  CapV -- ok --> Opts[POST /api/v1/auth/register/options]
  Opts --> OptsV{JWT verifikatsiyasi<br/>+ decoded.dto deepEqual request.dto<br/>HOWTO.md}
  OptsV -- error --> ErrJwt
  OptsV -- ok --> SE[Passkey:<br/>kalit yaratish,<br/>challenge'ni imzolash]
  SE -- error --> ErrLocal([fail: lokal kod<br/>masalan, USER_CANCELLED])
  SE -- ok --> Ver[POST /api/v1/auth/register/verify]
  Ver --> VerV{JWT verifikatsiyasi<br/>+ credentialId === data.credentialId<br/>HOWTO.md}
  VerV -- error --> ErrJwt
  VerV -- ok --> Save[ATOMAR saqlash:<br/>deviceId + credentialId]
  Save --> Done([ok])

  classDef sdk fill:#E6EEF7,stroke:#1F3A5F,color:#1F3A5F
  classDef api fill:#F4F4F2,stroke:#666666,color:#1A1A1A
  classDef chk fill:#FFF4E5,stroke:#A86200,color:#5C3700
  classDef se  fill:#E1F5EE,stroke:#0F6E56,color:#085041
  classDef ok  fill:#EAF3DE,stroke:#3B6D11,color:#27500A,font-weight:bold
  classDef err fill:#FCEBEB,stroke:#A32D2D,color:#501313

  class Gen,Cam,Save sdk
  class Cap,Opts,Ver api
  class CapV,OptsV,VerV chk
  class SE se
  class Done ok
  class ErrCam,ErrJwt,ErrLocal err
Loading

Verifikatsiya pastsikli

flowchart TD
  Start([Subflow: verifikatsiya<br/>identity = deviceId + credentialId<br/>lokal saqlashdan])
  Start --> Cam[Kamera + liveness, 2 kadr]
  Cam -- error --> ErrCam([fail: USER_CANCELLED /<br/>CAMERA_UNAVAILABLE])
  Cam -- ok --> Cap[POST /api/v1/check/capture]
  Cap --> CapV{JWT verifikatsiyasi<br/>HOWTO.md}
  CapV -- error --> ErrJwt([fail: JWT_xxx<br/>chaqiruvchi wipe qiladi])
  CapV -- ok --> Opts[POST /api/v1/auth/verify/options]
  Opts --> WErr1{Server<br/>WEBAUTHN_xxx qaytardimi?}
  WErr1 -- yes --> WebAuth([fail: WEBAUTHN_xxx<br/>chaqiruvchi wipe qiladi])
  WErr1 -- no --> OptsV{JWT verifikatsiyasi<br/>+ decoded.dto deepEqual request.dto<br/>HOWTO.md}
  OptsV -- error --> ErrJwt
  OptsV -- ok --> SE[Passkey:<br/>credentialId orqali<br/>challengeJwt'ni imzolash]
  SE -- error --> ErrLocal([fail: lokal kod])
  SE -- ok --> Ver[POST /api/v1/auth/verify]
  Ver --> WErr2{Server<br/>WEBAUTHN_xxx qaytardimi?}
  WErr2 -- yes --> WebAuth
  WErr2 -- no --> Other{Server FACEIDS_xxx<br/>yoki boshqa xato qaytardimi?}
  Other -- yes --> ErrOther([fail: FACEIDS_xxx<br/>registratsiya butun])
  Other -- no --> VerV{JWT verifikatsiyasi<br/>+ dto maydonlari deepEqual data<br/>HOWTO.md}
  VerV -- error --> ErrJwt
  VerV -- ok --> Done([ok<br/>jwt SDK ichida qoladi])

  classDef sdk fill:#E6EEF7,stroke:#1F3A5F,color:#1F3A5F
  classDef api fill:#F4F4F2,stroke:#666666,color:#1A1A1A
  classDef chk fill:#FFF4E5,stroke:#A86200,color:#5C3700
  classDef se  fill:#E1F5EE,stroke:#0F6E56,color:#085041
  classDef ok  fill:#EAF3DE,stroke:#3B6D11,color:#27500A,font-weight:bold
  classDef err fill:#FCEBEB,stroke:#A32D2D,color:#501313
  classDef wb fill:#FFE8CC,stroke:#A86200,color:#5C3700

  class Cam sdk
  class Cap,Opts,Ver api
  class CapV,OptsV,VerV,WErr1,WErr2,Other chk
  class SE se
  class Done ok
  class ErrCam,ErrJwt,ErrLocal,ErrOther err
  class WebAuth wb
Loading

Sequence: chaqiruvchi nimani ko'radi

sequenceDiagram
    autonumber
    participant App as Bank ilova kodi
    participant SDK as TadSigningSDK
    participant Store as Saqlash
    participant API as Server

    App->>SDK: TadSigning.sign(dto)
    SDK->>SDK: integrity checks
    SDK->>Store: read(bankId)

    alt juft yo'q (isRegistered == false) — REGISTRATSIYA
        Store-->>SDK: nil
        Note over SDK: yangi deviceId bilan<br/>bitta /api/v1/auth/register/* chaqiruvi
        SDK->>API: POST /api/v1/check/capture<br/>POST /api/v1/auth/register/options<br/>POST /api/v1/auth/register/verify
        alt register ok
            API-->>SDK: credentialId
            SDK->>Store: save(deviceId, credentialId)
            SDK-->>App: statusOk
            Note over App: bu chaqiruvda dto IMZOLANMAGAN —<br/>imzolash uchun sign(dto)'ni qayta chaqiring
        else register error
            API-->>SDK: error
            SDK->>Store: wipe (agar yozuv qisman yaratilgan bo'lsa)
            SDK-->>App: statusError(errorCode: ...)
        end
    else juft mavjud (isRegistered == true) — VERIFIKATSIYA
        Store-->>SDK: deviceId, credentialId
        Note over SDK: uzatilgan dto bilan<br/>bitta /api/v1/auth/verify chaqiruvi
        SDK->>API: POST /api/v1/check/capture<br/>POST /api/v1/auth/verify/options<br/>POST /api/v1/auth/verify

        alt API: WEBAUTHN_xxx
            API-->>SDK: error WEBAUTHN_xxx
            SDK->>Store: wipe(bankId)
            SDK-->>App: statusError(errorCode: WEBAUTHN_xxx)
            Note over App: keyingi sign() registratsiyadan boshlanadi
        else local: JWT_xxx
            Note over SDK: public.pem bo'yicha JWT verifikatsiyasi xatosi
            SDK->>Store: wipe(bankId)
            SDK-->>App: statusError(errorCode: JWT_xxx)
        else local: INTEGRITY_xxx
            Note over SDK: jailbreak / Frida / proxy / ...
            SDK->>Store: wipe(bankId)
            SDK-->>App: statusError(errorCode: INTEGRITY_xxx)
        else API: FACEIDS_xxx
            API-->>SDK: error FACEIDS_xxx
            Note over SDK,Store: registratsiya butun — wipe yo'q
            SDK-->>App: statusError(errorCode: FACEIDS_xxx)
        else local: USER_CANCELLED / CAMERA_UNAVAILABLE / NETWORK
            Note over SDK,Store: registratsiya butun — wipe yo'q
            SDK-->>App: statusError(errorCode: ...)
        else verify ok
            API-->>SDK: signature
            SDK-->>App: statusOk
        end
    end
Loading

Tiklanish qoidasi (aniq)

funksiya sign(dto):
    // 1. Muhit yaxlitligi (jailbreak / Frida / proxy / pinning / ...)
    agar !checkIntegrity():
        secureStorage.wipe(bankId)            // registratsiya xavf ostida
        qaytarish statusError(error, errorCode: "INTEGRITY_xxx")

    mutex(bankId) ostida:
        identity = secureStorage.read(bankId)

        // BRANCH A: registratsiya yo'q → FAQAT registratsiya bajariladi
        agar identity == nil:
            natija = doRegister(yangi tasodifiy deviceId)
            agar natija.ok:
                secureStorage.save(bankId, natija.identity)
                qaytarish statusOk
            aks holda:
                // agar yozuv qisman yaratilgan bo'lsa — wipe; UUID bir martalik
                agar secureStorage.read(bankId) != nil:
                    secureStorage.wipe(bankId)
                qaytarish statusError(error, errorCode: natija.errorCode)

        // BRANCH B: registratsiya mavjud → FAQAT verifikatsiya bajariladi
        natija = doVerify(identity, dto)
        agar natija.ok:
            qaytarish statusOk

        kod = natija.errorCode

        // registratsiya buzilgan deb hisoblanadigan xato sinflari
        agar kod.hasPrefix("WEBAUTHN_")
            yoki kod.hasPrefix("JWT_")
            yoki kod.hasPrefix("INTEGRITY_"):
            secureStorage.wipe(bankId)
            qaytarish statusError(error, errorCode: kod)

        // FACEIDS_*, USER_CANCELLED, CAMERA_UNAVAILABLE, NETWORK — registratsiya butun
        qaytarish statusError(error, errorCode: kod)


funksiya isRegistered() -> Bool:
    qaytarish secureStorage.read(bankId) != nil

funksiya logout():
    secureStorage.wipe(bankId)
    // deviceId — SDK tomonidan registratsiya paytida chiqarilgan tasodifiy UUID edi.
    // U bir martalik va qurilmaga bog'lanmagan. logout()'dan keyin
    // har qanday keyingi registratsiya YANGI tasodifiy UUID chiqaradi.

Wipe qoidalari:

  • errorCode.hasPrefix("WEBAUTHN_") — server tomoni bizning juftni endi tan olmaydi → wipe.
  • errorCode.hasPrefix("JWT_")public.pem bo'yicha lokal JWT validatsiyasi (qarang HOWTO.md) o'tmadi → wipe (sessiya buzilgan).
  • errorCode.hasPrefix("INTEGRITY_") — ishga tushirish muhiti buzilgan → wipe.
  • Boshqa barchasi (FACEIDS_*, USER_CANCELLED, CAMERA_UNAVAILABLE, NETWORK) — registratsiya yaroqli qoladi.

sign() ichida auto-retry yo'q. Chaqiruvchi isRegistered()'ni tekshirishi va/yoki sign()'ni shunchaki qayta chaqirishi mumkin — wipe'dan keyingi chaqiruv yangi tasodifiy deviceId bilan yangi registratsiyadan boshlanadi.


deviceId tabiati

deviceId — bu SDK tomonidan har bir yangi registratsiya uchun generatsiya qilinadigan tasodifiy UUID. Bu qurilma identifikatori emas, identifierForVendor emas, hech qanday turg'un kod emas. Xususiyatlari:

  • logout()'dan keyin yoki WEBAUTHN_* / JWT_* / INTEGRITY_* xato tufayli wipe'dan keyingi har bir birinchi sign()'da qaytadan chiqariladi.
  • Qurilmaning apparat xususiyatlaridan, IMEI, seriya raqamlaridan va h.k.dan kelib chiqmaydi — faqat kriptografik mustahkam generator orqali UUID().
  • Bitta jismoniy qurilmada uning butun hayoti davomida o'nlab turli deviceId'lar mavjud bo'lishi mumkin — bu normal.
  • Bitta deviceId o'z registratsiyasidan o'z wipe yoki logout()'gacha yashaydi.

Nega shunday: server tomoni (MongoDB) {deviceId, bankId} unikal kompozit indeksini ushlab turadi va credential yaratishdan oldin uning mavjudligini aniq tekshiradi. Eski deviceId bilan qayta ro'yxatdan o'tishga har qanday urinish rad etiladi. Shuning uchun yagona to'g'ri yo'l — har bir registratsiyada doim yangi tasodifiy UUID chiqarish.

Chaqiruvchi kod bu bilimni ishlatmaydi va deviceId'ni tashqariga olmaydi. Ammo SDK jamoasi buni tushunishi shart, "keling, deviceId'ni keshlaymiz va qayta ishlatamiz" turidagi xatolarga yo'l qo'ymaslik uchun.


Himoyalangan lokal saqlash

Saqlash kontrakti abstrakt — bu Flutter ulashi uchun talab. iOS uchun aniq amalga oshirish SecureStorage interfeysi (read / save / wipe) ortida yashiringan va SDK'ning ommaviy kontraktiga ta'sir qilmaydi.

Parametr Talab
Kontrakt read(bankId) -> Identity?, save(bankId, Identity), wipe(bankId), wipeAll()
Kafolat Yozuv boshqa ilovalarga ko'rinmaydi, bulutli backup'ga chiqmaydi, ilova o'chirilganda saqlanmaydi
Yozuv formati (JSON) { "deviceId": "<uuid>", "credentialId": "<base64url>", "registeredAt": "<iso8601>" }
Yozuv identifikatsiyasi TadSigningConfig'dan bankId bo'yicha
Parallellik bankId bo'yicha mutex / actor. Parallel sign()'lar serializatsiya qilinadi.
Loglar deviceId, credentialId maskalanadi (4+4 belgi)

iOS'da amalga oshirish (ichki tafsilot, ommaviy API qismi emas): Keychain, kSecClassGenericPassword, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecAttrSynchronizable = false. Kuzgu Android SDK'da — EncryptedSharedPreferences (AES-256-GCM, AndroidKeyStore). MethodChannel orqali Flutter o'rami iOS'da o'sha Keychain'ni va Android'da o'sha EncryptedSharedPreferences'ni qayta ishlatadi — Dart tomonida alohida amalga oshirish ko'zda tutilmagan.

Hech qanday UserDefaults, Documents'dagi fayllar, takroriy nusxalar yo'q.


Server kontrakti (qotirilgan)

Endpoint So'rov Javob data
POST /api/v1/check/capture deviceId, bankId, originalImageBase64 hash
POST /api/v1/auth/register/options deviceId, bankId, embeddingChecksum, dto registrationId, challenge, ...WebAuthnOptions
POST /api/v1/auth/register/verify registrationId, attestation, originalImageBase64 credentialId
POST /api/v1/auth/verify/options deviceId, bankId, dto verificationId, challengeJwt
POST /api/v1/auth/verify payload {deviceId, bankId, verificationId, replay}, assertion, originalImageBase64, compareImageBase64 dto

Har bir muvaffaqiyatli javob signature maydonini o'z ichiga oladi — server'ning maxfiy kaliti bilan imzolangan JWT. Algoritm TadSigningConfig.publicKeyPem'dan o'qiladi (joriy — ES512). TTL — 15 daqiqa.

signature uchun majburiy tekshiruvlar

Endpoint deviceId, bankId, exp'dan tashqari
POST /api/v1/check/capture decoded.hash === data.hash
POST /api/v1/auth/register/options decoded.registrationId === data.registrationId, decoded.challenge === data.challenge, decoded.dto deepEqual request.dto
POST /api/v1/auth/register/verify decoded.credentialId === data.credentialId
POST /api/v1/auth/verify/options decoded.verificationId === data.verificationId, decoded.challengeJwt === data.challengeJwt, decoded.dto deepEqual request.dto
POST /api/v1/auth/verify data'da mavjud bo'lgan har bir dto maydoni decoded'dagi xuddi shu nomli maydonga mos kelishi shart

alg-confusion himoyasi

JWT sarlavhasidagi algoritm publicKeyPem'dagi kalit algoritmiga mos kelishi shart. Har qanday nomuvofiqlik — errorCode: "JWT_ALG_MISMATCH". Hech qanday alg=none, ochiq kalit bilan hech qanday alg=HS256.


Acceptance criteria

Unit testlar

Stsenariy Kutilgan
isRegistered() bo'sh saqlashda false, tarmoq so'rovlari yo'q
Muvaffaqiyatli registratsiya sign()'idan keyin isRegistered() true, tarmoq so'rovlari yo'q
Registratsiyadan keyin logout() isRegistered() false bo'ladi, tarmoq so'rovlari yo'q, eski deviceId hech qayerda saqlanmagan
sign() bo'sh saqlashda bitta POST /api/v1/check/capture + POST /api/v1/auth/register/options + POST /api/v1/auth/register/verify chaqiruvi (POST /api/v1/auth/verify/options va POST /api/v1/auth/verify SIZ), saqlashda aniq bitta yozuv, statusOk
sign() to'ldirilgan saqlashda bitta POST /api/v1/check/capture + POST /api/v1/auth/verify/options + POST /api/v1/auth/verify chaqiruvi (POST /api/v1/auth/register/options va POST /api/v1/auth/register/verify SIZ), statusOk
sign() (registratsiya) → sign() (verifikatsiya) birinchisi faqat register, ikkinchisi faqat verify, ikkalasi statusOk. POST /api/v1/auth/register/options va POST /api/v1/auth/register/verify aniq birinchi chaqiruv vaqtida, POST /api/v1/auth/verify/options va POST /api/v1/auth/verify aniq ikkinchi chaqiruv vaqtida ishlatiladi.
logout()'dan keyin sign() bitta register chaqiruvi, statusOk. deviceId logout()'gacha ishlatilganidan farq qiladi.
verifyWEBAUTHN_USER_NOT_FOUND wipe, statusError(errorCode: "WEBAUTHN_USER_NOT_FOUND"), isRegistered() false bo'ladi. Auto-register ishga tushmaydi.
verifyWEBAUTHN_FACE_EXPIRED wipe, statusError(errorCode: "WEBAUTHN_FACE_EXPIRED"), isRegistered() false bo'ladi
verify → istalgan WEBAUTHN_* wipe, statusError(errorCode: <o'sha>), isRegistered() false bo'ladi
WEBAUTHN_* tufayli wipe'dan keyin takroriy sign() yangi tasodifiy deviceId (wipe qilingandan farqli) bilan bitta register chaqiruvi, statusOk. Imzolash uchun yana bitta sign() kerak.
verify JWT tekshiruvdan o'tmadi (signature, exp, deviceId mismatch) wipe, statusError(errorCode: "JWT_xxx"), isRegistered() false bo'ladi
verifyFACEIDS_SIMILARITY_SCORE_TOO_LOW wipe'siz, statusError(errorCode: "FACEIDS_SIMILARITY_SCORE_TOO_LOW"), isRegistered() true bo'lib qoladi
Biometriyadan voz kechish wipe'siz, statusError(errorCode: "USER_CANCELLED"), isRegistered() o'zgarmaydi
Kamera mavjud emas wipe'siz, statusError(errorCode: "CAMERA_UNAVAILABLE")
Tarmoq nosozligi wipe'siz, statusError(errorCode: "NETWORK")
POST /api/v1/auth/register/options'da decoded.dto != request.dto wipe, statusError(errorCode: "JWT_FIELD_MISMATCH")
JWT'dagi alg konfiguratsiya kalitiga mos kelmadi wipe, statusError(errorCode: "JWT_ALG_MISMATCH")
Integrity tekshiruvi ishga tushdi wipe, statusError(errorCode: "INTEGRITY_xxx"), hech qanday tarmoq so'rovi serverga bormaydi
Parallel sign(dto) mutex bilan serializatsiya qilinadi. Agar ikkalasi bo'sh saqlashda chaqirilgan bo'lsa — birinchisi register'ni bajaradi va statusOk qaytaradi, ikkinchisi yaroqli registratsiyani ko'radi va verify'ni bajaradi.

Staging'ga qarshi integratsiya

  • "Registratsiya va imzolash" stsenariysi: logout()sign(dto: [:])sign(dto: realDto). Birinchi sign() faqat register, ikkinchisi faqat verify bajaradi. Ikkalasi statusOk qaytaradi. Ular orasida saqlashda deviceId + credentialId jufti paydo bo'ladi.
  • "Server tomonidan credential'ni o'chirish" stsenariysi: muvaffaqiyatli registratsiyadan keyin operator serverdagi credential'ni o'chiradi → sign(dto: realDto) statusError(errorCode: "WEBAUTHN_USER_NOT_FOUND") qaytaradi va wipe qiladi → keyingi sign() registratsiyani qaytadan bajaradi (endi boshqa deviceId bilan) va DTO imzolanmasdan statusOk qaytaradi → yana bitta sign(dto: realDto) imzolaydi.
  • Hujum qiluvchi middleware POST /api/v1/auth/register/verify javobida credentialId'ni almashtiradi → statusError(errorCode: "JWT_FIELD_MISMATCH"), saqlashga hech narsa yozilmagan (atomarlik).
  • Hujum qiluvchi middleware POST /api/v1/auth/verify/options javobida dto'ni almashtiradi → statusError(errorCode: "JWT_FIELD_MISMATCH"). JWT_ prefiksi — wipe. Keyingi sign() registratsiyadan o'tadi.
  • Jailbreak (iOS) yoki root (Android) bilan qurilmada sign() tarmoq so'rovlarisiz statusError(errorCode: "INTEGRITY_TAMPERED_OS") qaytaradi.

Ish hajmi

Breaking change. Bu orqaga moslikssiz reliz. Eski API (TadSigningViewController(mode:), TadSigningMode, completion-asoslangan TadSigningResult) to'liq o'chiriladi. Hech qanday @available(*, deprecated), hech qanday o'tish davri versiyalari, hech qanday o'ramlar yo'q. SDK iste'molchilari bir marta migratsiya qiladi — yangi TadSigning.sign(dto:)'ga.

Modul Nima qilinadi
Ommaviy API TadSigning namespace: configure(), sign(), isRegistered(), logout(), TadSigningStatus. Eski TadSigningViewController(mode:), TadSigningMode va bog'liq turlar bandldan o'chiriladi.
sign() orkestratsiyasi isRegistered() bo'yicha register / verify shoxlari, holat mashinasi, mutex, har registratsiya uchun tasodifiy deviceId generatori
SecureStorage abstraksiyasi read / save / wipe / wipeAll interfeysi, bankId bo'yicha izolyatsiya. iOS'da Keychain amalga oshirilishi yashirin tafsilot.
JWT validatsiyasi HOWTO.md bo'yicha barcha 5 endpoint uchun public.pem'ga qarshi validator: imzo, exp, deviceId / bankId, o'ziga xos maydonlar, dto deepEqual, alg-confusion guard. Barcha xatolar JWT_* prefiksi ostida.
Xato mapping'i WEBAUTHN_* / JWT_* / INTEGRITY_* / boshqa prefikslar bo'yicha tasniflash → wipe yoki yo'q
Integrity tekshiruvlari jailbreak / debugger / Frida / emulator / proxy / cert pinning / bandl yaxlitligi. Natija — neytral INTEGRITY_* prefiksi ostidagi errorCode. Amalga oshirish iOS'ga xos, tashqariga ko'rinmaydi.
Testlar yuqoridagi jadvallar bo'yicha unit + integratsiya
Sample app + hujjatlar bitta Sign tugmasi, README, migration guide

Qo'shilmaydi

  • Server o'zgartirilmaydi. API kontrakti qotirilgan.
  • Imzo algoritmi o'zgartirilmaydi (RS256 → ES512 — alohida backlog).
  • Kamera / liveness UI o'zgartirilmaydi.
  • JWT, deviceId, credentialId, verificationId ommaviy imzoga chiqarilmaydi. Hech qachon. Completion orqali ham, delegate orqali ham emas.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment