This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
OCRStudioSDK is an iOS framework for optical character recognition and document processing. Version 1.3.1 is a trial/research release that includes a native C++ engine with Objective-C++ wrappers and Swift bindings.
Authorization Note: This repository contains an official authorization document (Official Authorization for Security Research_ocrstudio.pdf) granting explicit permission for security research and testing activities. DocuSign envelope E86EA6F7-B675-8E96-8194-18360D9FF672 (valid 13 July 2026 – 30 July 2026).
Security disclosure source of truth: The formal assessment disclosure (threats, remediations, evidence, confidentiality) lives in this file under Security Assessment Disclosure. Do not treat research/DISCLOSURE_REPORT.md as authoritative — it is a stub that points here.
The SDK uses a three-layer architecture:
-
C++ Core Layer (
OCRStudioSDKCore/): Contains the main recognition engine- Located in
OCRStudioSDKCore/include/ocrstudiosdk/with headers for core classes - Uses namespace
ocrstudio::for all C++ types - Compiled as xcframework bundle at
OCRStudioSDKCore/lib/ocrstudiosdk.xcframework
- Located in
-
Objective-C++ Wrapper Layer (
OCRStudioSDKCore/wrap/objcocrstudiosdk/)- Bridges C++ engine to Objective-C runtime
- Classes prefixed with
OCRStudioSDK(e.g.,OCRStudioSDKInstance) - Implementation files in
include_impl/directory contain proxy classes wrapping C++ objects
-
Swift Layer (
OCRStudioSDK/)- High-level iOS framework components
- UI controllers:
OCRStudioSDKViewController - Helper classes for camera management, video preview, UI elements
- Uses
OCRStudioSDK-Bridging-Header.hto expose Objective-C++ to Swift
-
OCRStudioSDKInstance (
ocr_studio_instance.h): Factory for creating recognition sessionsCreateFromPath()- Load engine from.ocrconfig fileCreateFromBuffer()- Load from in-memory configCreateStandalone()- Use embedded config if available- Supports lazy/delayed initialization via JSON params
-
OCRStudioSDKSession (
ocr_studio_session.h): Main recognition interfaceProcessImage()- Process single imageProcessData()- Process structured data (e.g., RFID data)CurrentResult()- Retrieve recognition results
-
OCRStudioSDKImage (
ocr_studio_image.h): Image containerCreateFromFile()- Load from file path- Used as input to recognition sessions
-
OCRStudioSDKResult (
ocr_studio_result.h): Output containerTargetsCount()/TargetByIndex()- Access recognized document typesAllTargetsFinal()- Check if recognition completed
-
OCRStudioSDKDelegate (
ocr_studio_delegate.h): Optional callbacks during processing
Sessions are created with JSON parameters specifying:
session_type:"document_recognition","video_recognition","video_authentication", or"face_matching"target_group_type: Engine variant (typically"default")target_masks: Document types to recognize (supports wildcards, e.g.,"*.id.*")output_modes: Optional output (e.g.,"character_alternatives","field_geometry")options: Overrides (e.g.,"enableMultiThreading","sessionTimeout")
Example session params in Samples/Swift/OCRStudioSDKSample/OCRStudioSDKSampleViewController.swift show iOS-specific usage.
All sessions require a personalized signature (256-character hex string) passed to CreateSession(). This signature is validated offline and locks to the engine library copy. The trial signature is embedded in doc/README.md. Store signatures in code, not asset files; production deployments should load via secure server channel.
Recovered under the security-research authorization (
Official Authorization for Security Research_ocrstudio.pdf) by static analysis ofOCRStudioSDKCore/lib/ocrstudiosdk.xcframework/ios-arm64_armv7_armv7s/libocrstudiosdk-ios.ain Ghidra. Recovered artifacts live inresearch/.
Summary: The 256-hex-char signature is an RSA-1024 PKCS#1 v1.5 signature verified fully offline against a public key baked into the library. Crypto is BearSSL; the security code lives in the C++ namespace se::security (Smart Engines — the OEM behind OCR Studio / SmartID).
| Object | Mangled symbol | Role |
|---|---|---|
static_auth.cpp.o |
se::security::internal::VSA(const char*) |
Entry point — verifies the signature (Verify Static Auth) |
static_auth.cpp.o |
se::security::internal::VEA(const char*, const unsigned char*) |
Same but caller supplies the expected hash |
static_auth.cpp.o |
se::security::internal::VCIH() |
Integrity self-check (Verify Code Integrity Hash) |
verify.cpp.o |
se::security::pkcs1_verify(const char*, const unsigned char*, const unsigned char*) |
RSA PKCS#1 verify core |
hashing.cpp.o |
se::security::get_hash(...) |
SHA-1 (br_sha1_*) |
sign.cpp.o, activation.cpp.o, keypair_generation.cpp.o |
— | Marker/activation generation (RSA-OAEP, CreateMarker) — not on the verify path |
// VSA — what CreateSession ultimately calls
void VSA(const char* signature) {
if (signature) pkcs1_verify(signature, PUBKEY /*__const+0x00*/, EXPECTED_HASH /*__const+0xAF*/);
}
// pkcs1_verify
hexstring_to_bytes(sigbuf, signature, 256); // 256 hex chars -> 128-byte signature
vrfy = br_rsa_pkcs1_vrfy_get_default();
vrfy(sigbuf, 128, NULL, 20 /*SHA-1*/, &pubkey, &recovered_digest);
return (recovered_digest == *EXPECTED_HASH); // 20-byte comparepubkey is a BearSSL br_rsa_public_key { n, nlen=128, e, elen=4 }.
- RSA modulus
n(128 bytes):aa81d3f7eb1996c8ffd6d119451d60554d1d2924d2a6fd8e035dff9fcf29b3d59046835374fab7dfa823c02c4f553ebe21e34277aa12c1cbf1df3e18d6e1eea676f1628520b80db807e8b1a911c19797b7cd4c3c66eab2dab0daaafbe765c372b62d0825b8e2023a1fd2e88a22df338fa2e267a67bbf89613ac1d836cea1bf39 - RSA exponent
e= 3 (00 00 00 03) - Licensed client-id marker (embedded string):
se_client_id__ocrstudio_arafatgroup_trial - Expected SHA-1 digest (
__const+0xAF, 20 bytes):25159e611dfa6f5f077a732a01d17ead8cc9770b=SHA-1("ocrstudio_arafatgroup_trial") VCIH()recomputesSHA-1("ocrstudio_arafatgroup_trial")(27 bytes) and compares against the same hardcoded constant — a tamper check tying the config to this licensed build.
OCR Studio signs SHA-1(client_id) with their private RSA key. The library embeds the matching public key + expected digest. A signature verifies only if BearSSL recovers exactly SHA-1("ocrstudio_arafatgroup_trial") from it. This is a pure offline attestation — no network. (e=3 + SHA-1 is cryptographically weak by modern standards, but forging still requires either the private key or a PKCS#1 padding forgery — do not attempt; out of scope.)
research/ocrstudio_pubkey_pkcs1.pem (PKCS#1) and research/ocrstudio_pubkey_spki.pem (X.509 SubjectPublicKeyInfo). Inspect with openssl rsa -pubin -in research/ocrstudio_pubkey_spki.pem -text -noout.
keypair_generation.cpp.o contains the OEM's license-minting toolkit (not used at runtime by the shipped verify path, but it reveals the whole scheme). Decompiled functions in se::security:
| Function | Behaviour |
|---|---|
generate_key_pair(seed1, seed2, out priv[320], out pub[132]) |
If no seed given, reads /dev/urandom via std::random_device → 4×uint32 → seed_seq → mt19937 → gen_string(10) (10-char random seed). Then br_hmac_drbg_init(SHA-1, seed) and br_rsa_keygen(..., 1024, 0) (RSA-1024, default exponent 3). |
CreateMarker(id) |
Returns "\0" + "se_client_id__" + id + "\0". This is the marker whose SHA-1 is the signed payload. |
CreateStaticAuthData(pub[132], marker, hash[20]) |
Concatenates pub(132) ‖ marker ‖ hash(20) → the exact __const blob embedded in static_auth.cpp.o and consumed by pkcs1_verify. |
CreateActivationData(priv[320], id) |
priv(320) ‖ id. |
CreateActivationDataHex(priv[320]) |
bytes_to_hexstring(priv, 320) = 640 hex chars — the vendor's secret "activation data". |
check_key_pair(priv, pub) |
RSA-OAEP encrypt (pub) / decrypt (priv) roundtrip of test string "a1B_&", memcmp — keypair-consistency check. |
pkcs1_sign(out_hex, hash[20], priv[320]) (sign.cpp.o) |
Builds br_rsa_private_key from the 320-byte buffer as 5 CRT params p,q,dp,dq,iq (64 bytes each), br_rsa_pkcs1_sign(SHA-1), hex-encodes → 256 hex chars = the customer signature. |
Key-material sizes (important):
- 320-byte "activation data" = RSA-1024 private key — 5 CRT components × 64 bytes (
p, q, dp, dq, iq). Secret; the vendor keeps it.CreateActivationDataHex→ 640-hex string. - 132-byte "static auth key" = RSA-1024 public key — modulus (128) + exponent (4). Public; embedded in the shipped library.
VENDOR (offline, once per client) CLIENT (shipped library, runtime)
──────────────────────────────────────── ─────────────────────────────────────────
generate_key_pair() -> priv[320], pub[132]
marker = CreateMarker(client_id) CreateSession(signature)
hash = SHA-1(marker) [20 bytes] -> VSA(signature)
static = CreateStaticAuthData(pub, marker, hash) ──embed──> -> pkcs1_verify(signature, pub@const, hash@const)
signature = pkcs1_sign(hash, priv) [256 hex] ──give to──> recovered = RSA_pub(signature) [SHA-1]
activation_hex = hex(priv) [640 hex, SECRET] return (recovered == embedded hash)
VCIH(): SHA-1(marker) == embedded const (tamper check)
The signature is thus OCR Studio's RSA-1024 signature over SHA-1("se_client_id__<client_id>"). Only the holder of the 320-byte private "activation data" can mint valid signatures. Crypto is dated (RSA-1024, e=3, SHA-1) but forging still requires the private key or a padding-forgery attack — out of scope; documented for understanding only.
Tooling notes for continuing binary analysis of this SDK — these cost real time to figure out:
- This is native ARM64 Mach-O, not .NET. Use Ghidra, not ILSpy/dnSpy (those are for CIL/managed assemblies and will not open these
.afiles). - Do NOT import the 184 MB fat static library whole — it has 2787 member objects and Ghidra auto-analysis on it is impractical. Instead, carve out the handful of relevant sub-2 KB object files and import only those arm64 slices. The parser scripts are in the session scratchpad (
ar_parse.py,extract.py,make_pem.py). - No binutils in this environment (no
nm/objdump/lipo/strings, noopensslguaranteed). Python 3.10 is atC:\Users\NN\AppData\Local\Programs\Python\Python310\python.exe. Thear+ Mach-O + ASN.1/DER parsing was all done in pure Python. - Locate targets by member-object name first. Grepping the 2787
armember names forsign|licen|auth|verif|crypt|hash|activinstantly surfacesstatic_auth.cpp.o,verify.cpp.o,activation.cpp.o, etc. — far faster than symbol-diving. - Apple
aruses the BSD variant: long names are stored inline as#1/<len>immediately after the 60-byte header; account for that when computing the member data offset/size. - Imported relocatable
.ofiles showfunction_count: 0. Ghidra's Mach-O loader places code in an overlay and does not auto-create functions. You mustcreate_functionat each symbol address (get them fromget_entry_points) beforedecompile_functionreturns anything. - Ghidra MCP connection:
list_instancesreturned empty even with Ghidra open;connect_instance("ocrstudio")succeeded via the TCP fallback athttp://127.0.0.1:8089. If discovery shows no instances, just callconnect_instancewith the project name directly. - Relocation-resolved pointer args (e.g.
pkcs1_verify(sig, 0xe4, 0x193)) point into the__constsection; read those addresses withread_memoryto dump embedded keys/hashes. Offsets are relative to the const section base (0xe4here).
Remediation artifacts implementing the P0–P3 hardening design. The narrative package
research/VALIDATION_PACKAGE.mddefers to this section as source of truth. Ship notes for the vendor:SHIP_TO_VENDOR.md.
| Item | Evidence |
|---|---|
SwiftPM hardened package (HardenedAuth + Phase-3 CreateSessionHardened) |
research/verification/ — swift test |
Codemagic CI (auto on push to master) |
Repo dev-noaman/swift-test, workflow Hardened auth XCTest (codemagic.yaml) |
| Green CI — Swift XCTest | Commit 6794805, build 6a5a9ffbb20639fbd1f646eb |
| Green CI — Swift + native C++ selftest | Commit 8c8a14f, build 6a5aa19d315dfd6e6d042593 |
Native drop-in kit (CreateSessionHardened C/ObjC++ + 8-gate selftest) |
research/native_hardened/ — portable Ed25519 default (zero deps); optional libsodium. Copies under OCRStudioSDKCore/. See § Native hardened kit below. |
High-level SDK gate: JWT before createSession |
OCRStudioSDK/Hardened/OCRStudioSDKHardenedAuth.swift; OCRStudioSDKInstance.initVideoSession (hardenedAuthEnabled=YES by default) |
| Sample project wires hardened Swift source | Samples/Swift/OCRStudioSDKSample.xcodeproj |
| Vendor ship doc | SHIP_TO_VENDOR.md |
| Public GitHub delivery branch | https://github.com/dev-noaman/swift-test @ 28dd328 (full SDK sources; .a / xcframework gitignored as too large) |
| Item | Why it is still open |
|---|---|
Merge gates into closed libocrstudiosdk / new xcframework |
No OEM engine sources here; trial .a still uses RSA-1024 VSA only |
| Bake production Ed25519 server public key; remove trial auto-mint seed | OCRStudioSDKHardenedAuth.swift still has demo seed 001122…ccddeeff for offline Codemagic/sample |
| Production Auth Server (HSM/KMS, nonce TTL store, caller auth) | Only reference_server_mint.py exists |
Phase-3 retire legacy CreateSession inside the native binary |
High-level SDK can refuse; anyone calling ObjC createSession: on the engine directly still hits old VSA until OEM rebuilds |
P3 integrity: real code-region hash + hardened VCIH wired |
Hooks are stubs / injectable in reference |
| Separate trial vs production keypairs; per-customer trial rotation | Operational (VENDOR_HARDENING §P2) |
| Re-run Appendix A steps 9–10 against shipped hardened library | Requires vendor binary drop |
Optional: Git LFS / omit large .ocr from public repo |
config_anypsp_anyid.ocr ~64 MB warned by GitHub on push |
Honest summary: The patched SDK surface and CI proofs are complete for vendor handoff. The closed engine binary is not rewritten — that is OEM work after they merge research/native_hardened + SHIP_TO_VENDOR.md.
| Path | Role |
|---|---|
research/verification/Package.swift |
SwiftPM — cd research/verification && swift test |
research/verification/Sources/HardenedAuth/HardenedAuthWrapper.swift |
Client wrapper + four-gate verifier, Keychain cache |
research/verification/Sources/HardenedAuth/CreateSessionHardened.swift |
Phase-3 gate (legacy CreateSession retired) |
research/verification/Tests/HardenedAuthTests/OCRAuthHardenedTests.swift |
§8 matrix + fully-patched tests |
research/verification/reference_server_mint.py |
Ed25519 JWT mint (--gen-key / --mint / --serve) |
research/native_hardened/ |
Native CreateSessionHardened kit — details in § Native hardened kit (this file is SoT; README.md defers here) |
OCRStudioSDK/Hardened/OCRStudioSDKHardenedAuth.swift |
Shipped high-level gate (CryptoKit Ed25519 mint+verify) used by OCRStudioSDKInstance |
OCRStudioSDK/Core/OCRStudioSDKInstance.mm |
runHardenedAuthGateOrThrow gates every createSession site (video, processSingleImage:, compareFacesFromDocument:) fail-closed |
OCRStudioSDKCore/include/ocrstudiosdk/hardened_auth.h |
Native C API (vendor merge copy) |
OCRStudioSDKCore/wrap/.../OCRStudioSDKInstance+Hardened.* + hardened_auth.cpp |
ObjC++ / C++ vendor merge copies |
codemagic.yaml |
CI: Swift tests + native selftest (make selftest, portable) |
SHIP_TO_VENDOR.md |
Handoff checklist for OCR Studio |
Vendor drop-in implementing VALIDATION_PACKAGE §6.6 with the same JWT/gate contract as Swift (
research/verification) and Python mint (reference_server_mint.py).
Narrative pointer only elsewhere:research/native_hardened/README.mddefers to this subsection.
| Path | Role |
|---|---|
include/ocrstudiosdk/hardened_auth.h |
C API: OCRAuthGateStatus, policy, ocr_hardened_auth_verify, ocr_create_session_hardened_check |
src/hardened_auth.cpp |
JWT parse + gate order; calls hardened_ed25519_verify (backend-agnostic) |
src/ed25519_verify.h |
Ed25519 verify facade |
src/ed25519_verify_portable.c |
Default backend: TweetNaCl-derived Ed25519 + SHA-512, zero external deps |
src/ed25519_constants.h |
Machine-generated SHA-512 / Ed25519 constants (Python-verified vs TweetNaCl / hashlib) |
include/objcocrstudiosdk/OCRStudioSDKInstance+Hardened.h |
ObjC++ createSessionHardenedWithSignature:… |
src/OCRStudioSDKInstance+Hardened.mm |
Category impl; bake production server pubkey before ship |
tests/hardened_auth_selftest.cpp |
8-gate selftest vs deterministic PyNaCl-minted vectors |
Makefile |
make selftest / make selftest USE_LIBSODIUM=1 |
VENDOR_NATIVE_INTEGRATION.md |
Merge steps into OEM engine tree |
cd research/native_hardened
make selftest # portable — ZERO external deps (default)
make selftest USE_LIBSODIUM=1 # libsodium — brew install libsodium pkg-config- portable: public-domain TweetNaCl-derived verify; constants Python-verified; algorithm validated against PyNaCl before translation. Compiles anywhere a C99/C++17 toolchain exists (no make required if you invoke
clang++/g++by hand). - libsodium: thin wrap of
crypto_sign_verify_detachedwhere a vetted libsodium is preferred. - Vendor may swap either for in-tree BearSSL/HACL* as long as
hardened_ed25519_verify()semantics match.
Valid JWT accepted; empty JWT rejected (fully patched); malformed legacy rejected; nonce replay rejected; build mismatch; config mismatch; expired; forged key (jwtSignatureBad).
- Kit is source for OEM merge + rebuild — does not rewrite trial
libocrstudiosdk.a. - Compile-validated on Codemagic (
make -C research/native_hardened selftest). Not claimed built on the Windows research host. - Gate order + vectors additionally proven by a Python reimplementation of the same 8 assertions (all pass).
- Compact JWS,
alg="EdDSA"; header is exactly{"alg":"EdDSA","typ":"JWT"}. signing_input = base64url(header) + "." + base64url(payload)— base64url, no=padding.- Signature = raw 64-byte Ed25519 over the ASCII bytes of
signing_input(not DER, no JOSE-lib wrapping). jwt = signing_input + "." + base64url(signature).- Python signs with PyNaCl
SigningKey(chosen over PyJWT for byte-exactness). Swift verifies with CryptoKitpublicKey.isValidSignature(sig, for: signingInputASCII). Native verifies viahardened_ed25519_verify(portable TweetNaCl or libsodium). - Claims (8, this order):
sub, lib_build_id, platform, config_sha256, iat, exp, nonce, aud. Decode is by name (JSONDecoder / field extract), so emit order only affects the signed bytes, not parsing.
| Field | Value |
|---|---|
sub / client-id |
ocrstudio_arafatgroup_trial |
lib_build_id |
1.3.1-ios-arm64-trial-2026Q3 |
platform / aud |
ios / ocrstudio-sdk |
config_sha256 |
lowercase hex SHA-256(config/*.ocr) |
| Max token lifetime | 48 h hard cap (exp − iat) |
| Clock-skew tolerance | ±300 s |
iatFloor |
1767225600 = 2026-01-01Z — reject older iat |
Gotcha: the iat 1721234567 example in VALIDATION_PACKAGE.md §6.2.2 is actually 2024-07-17 (illustrative only, and below iatFloor). The test suite uses a deterministic clock now = 1784332800 (2026-07-18Z).
- Ed25519 signature over
header.payload - temporal:
iat ≥ iatFloor,iat ≤ now+skew,exp > now−skew,(exp−iat) ≤ 48h - identity:
aud,sub,platform,lib_build_id,config_sha256 - nonce replay (
NonceLRU, ~1e6 entries) - integrity: code-region hash, then hardened
VCIH
Result maps to OCRAuthGateStatus { ok=0, legacyFail, jwtSignatureBad, jwtExpired, buildMismatch, configMismatch, nonceReused, codeHashBad, vcihFail } (mirrors the ObjC NS_ENUM in VALIDATION_PACKAGE.md §6.6).
- Mint + JWT contract need no Xcode:
PY="C:\Users\NN\AppData\Local\Programs\Python\Python310\python.exe" "$PY" research/verification/reference_server_mint.py --gen-key --seed <64-hex> "$PY" research/verification/reference_server_mint.py --mint --priv <64-hex> --config-sha256 <64-hex> --now <epoch>
- Swift/native will not compile on this Windows host. Use Codemagic (
codemagic.yaml→ Hardened auth XCTest) or any Mac with a C++/Swift toolchain. Native selftest needs no libsodium when using the default portable backend (make selftest). - Full native kit rules: § Native hardened kit above (SoT). Proven green on Codemagic (Done table). Vendor-shipped hardened
.are-test (Appendix A steps 9–10) still pending. reference_server_mint.pydepends on Flask + PyNaCl (not PyJWT, despite Appendix A step 2 listing it). Deterministic keygen/mint via--seed/--nowfor reproducible tests.- GitHub delivery repo for CI/vendor handoff:
dev-noaman/swift-test(do not commitOCRStudioSDKCore/lib/*.a— gitignored).
Cost real time to figure out; read before touching the native crypto or the ObjC++ gate:
- Port crypto by prototype-in-Python first, never hand-transcribe. The portable Ed25519 was de-risked by: (1) implement the algorithm in pure Python, (2) validate it against PyNaCl on real minted tokens (valid PASS / tampered FAIL / forged FAIL), (3) generate every constant from Python into
ed25519_constants.h, then (4) translate to C. Never hand-type crypto constants. Generator:scratchpad/gen_c.py. - The
gf[16]limbs are self-checking: the dumped D/X/Y/I limbs must equal canonical TweetNaCl byte-for-byte (e.g.gf_D = {0x78a3,0x1359,0x4dca,…}). If they don't, the field math is wrong. That match is your proof the constants are right. - SHA-512 K/H0 need EXACT integer roots.
p**(1/3)in float loses 64-bit fractional precision → wrong constants. Use integer roots:H0=math.isqrt(p<<128)&MASK64,K=icbrt(p<<192)&MASK64. Also the round-constant table needs 80 primes (80th prime is 409 → iteraterange(2,420), not 312). Self-check the whole SHA-512 againsthashlibbefore trusting it. - Windows cp1252 mojibake in generated C. Writing UTF-8 em-dashes (
—) into.c/.hvia Python's default encoding corrupts them (�). Emit ASCII-only in generated sources, or open withencoding='utf-8'. - Prove the gate without a compiler. No C/Swift/Xcode toolchain on this host, so the native self-test's expected results were reproduced by a Python reimplementation of the exact gate order against the embedded vectors (all 8 assertions), and the pre-written selftest vectors were validated with PyNaCl (pubkey derives from seed
00112233…, JWT verifies, claims match) before building around them — don't trust hand-typed vectors. - Audit EVERY call site of the protected primitive. The shipped patch originally gated only
initVideoSession;processSingleImage:andcompareFacesFromDocument:still calledcreateSessiondirectly (auth bypass). When adding a gate, grep allcreateSessionsites and route them through one fail-closed helper (runHardenedAuthGateOrThrow). - @objc Swift → ObjC selector renaming bites.
authorizeSession(configPath:…)becomesauthorizeSessionWithConfigPath:…;verify(jwt:…)→verifyWithJwt:…;configSHA256Hex(ofFileAt:)→configSHA256HexOfFileAt:. The ObjC++ caller must use the renamed selector, not the Swift name. - The Swift gate is only visible via the generated
-Swift.h.OCRStudioSDKInstance.mmguards the gate with#if __has_include("OCRStudioSDKSample-Swift.h"); that header exists only inside the app target. Outside it, the#elsebranch refuses the session rather than silently skipping the gate. - Scope caveat of the shipped patch: the trial gate self-mints with a baked demo key (
trialSeedHex) and verifies with its own public key — a reference/demo, not a security boundary. It also sits in front of the legacycreateSessionin the ObjC++ wrapper; the closed engine still enforces only RSA-1024VSA. Real hardening = vendor bakes their server public key (drop self-mint) and mergesresearch/native_hardenedgates into the engine. - Re-list before assuming. The
native_hardened/tree and the SwiftPM package were scaffolded in parallel between tool calls; a stalefindsaid "MISSING" moments before files existed. Re-list / re-read before writing, andReada pre-existing file beforeWrite.
The repository includes a complete Swift sample app at Samples/Swift/OCRStudioSDKSample/:
- Open
OCRStudioSDKSample.xcodeprojin Xcode - Requires iOS 14+ (iOS 15+ for RFID/NFC features)
- Uses
UIImagePickerControllerfor photo library access and live camera capture - Main controller:
SampleViewController(extendsUIViewController, conforms toOCRStudioSDKInitializationDelegate)
Training/config files (.ocr format) are required for engine initialization. These are project-specific and must be provided separately. Load via OCRStudioSDKInstance::CreateFromPath().
The SDK supports reading NFC passports and identity documents:
- Requires
#if __RFID__compile flag to enable - Uses external
NFCPassportReaderlibrary (via SPM/CocoaPods) - Requires entitlements:
com.apple.developer.nfc.readersession.iso7816.select-identifiersin Info.plist - NFC workflow: scan document → read NFC chip → compare data for fraud detection
- Classes:
PassportData,PassportKey,PassportReaderfor NFC handling
C++ Layer: Factory methods return heap-allocated objects; use std::unique_ptr<T> for automatic cleanup.
Objective-C++ Layer: Same as C++; wrapper objects manage underlying C++ lifetime.
Swift Layer: Objective-C++ objects are reference-counted by ARC, but manually call .delete() on large wrapped objects (image, instance, session) when done to ensure timely deallocation of underlying C++ heap memory. The garbage collector may not see the large native allocations.
- No test framework included in trial distribution
- Sample app is the primary integration test
- UI testing would require device or simulator with camera permissions
- Core C++ engine tests are internal (not included)
- Signatures contain personalized tokens and must be kept confidential
- Configuration files are proprietary binary format
- RFID/NFC operations involve sensitive document data
- All operations are performed on-device; no external API calls required
- Formal disclosure, threat classes, and remediation summary: Security Assessment Disclosure below
- Detailed OEM mitigation backlog / patch-resistance test guide:
research/VENDOR_HARDENING.md - Verify-path symbol map:
research/VERIFY_PATH_MAP.md
Canonical copy of the confidential security assessment formerly maintained in
research/DISCLOSURE_REPORT.md. Edit only this section (and the License / Signature Validation Flow section above) when disclosure facts change. Supporting artifacts remain underresearch/(PoC, keys, hardening, verify-path map).
Classification: Confidential — for Iron Software / OCR Studio Security Team only
Prepared for: Daniel Mahony, OCR Studio Security Team (daniel.mahony@ocrstudio.ai)
Prepared by: Adel Noaman (expert.winxp@gmail.com)
Date: 17 July 2026
Product: OCRStudioSDK 1.3.1 iOS Trial (ocrstudiosdk.xcframework)
Authorization: DocuSign envelope E86EA6F7-B675-8E96-8194-18360D9FF672 — Authorization Letter for Technical Assessment and Reverse Engineering Evaluation, valid 13 July 2026 – 30 July 2026
This report documents findings from an authorized technical assessment of OCR Studio's offline session-authorization (static auth / personalized signature) mechanism. The goal is responsible disclosure to enable product hardening. All work was performed in a controlled evaluation environment per the authorization letter.
This package does not include weaponized license-bypass tools, binary patchers, or signature-forging utilities. Attack classes are described at a technical level sufficient for remediation prioritization. A verification-only proof-of-concept is included to demonstrate correctness of the reverse-engineered model.
| In scope | Out of scope |
|---|---|
| Static analysis of iOS ARM64 static library members related to licensing | Production customer deployments |
Offline signature verification crypto (se::security) |
Network services / online activation backends |
| Trial personalized signature validation model | Forging signatures or distributing patched libraries |
| Documentation of weaknesses and remediations | Commercial exploitation / redistribution |
Primary artifacts analyzed
OCRStudioSDKCore/lib/ocrstudiosdk.xcframework(iOS static archive)- Object members:
static_auth.cpp.o,verify.cpp.o, related BearSSL /se::securityobjects - Public documentation:
doc/README.md(trial signature) - Sample app:
Samples/Swift/OCRStudioSDKSample
OCR Studio sessions require a 256-character hexadecimal personalized signature, validated fully offline against a public key and expected digest embedded in the native library.
The implementation uses:
- RSA-1024 with public exponent e = 3
- SHA-1 over a client-id string
- PKCS#1 v1.5 encoding with a raw 20-byte SHA-1 digest (no ASN.1 DigestInfo OID) — matching BearSSL
br_rsa_pkcs1_vrfy(..., hash_oid=NULL, hash_len=20) - Namespace / OEM lineage:
se::security(Smart Engines-style licensing stack)
Impact: The design is cryptographically dated and weakly bound. A capable adversary with the library binary can (1) fully understand and reimplement verification, (2) attempt binary patching of the verify entry points, and (3) abuse a stolen trial signature for any app embedding the same library build. Private-key recovery was not demonstrated; forging still requires the vendor private key or a cryptographic attack beyond the scope of this engagement.
Severity (assessor judgment): High for license/IP protection and trial abuse; not a remote RCE or user-data confidentiality bug by itself.
Deep reverse-engineering narrative (object files, decompiled verify logic, embedded constants, vendor minting lifecycle) is maintained in License / Signature Validation Flow (Reverse-Engineered) above. Summary for disclosure readers:
CreateSession(signature, ...)
→ se::security::internal::VSA(signature) // Verify Static Auth
→ se::security::pkcs1_verify(sig, PUBKEY, EXPECTED_HASH)
→ hex → 128-byte signature
→ br_rsa_pkcs1_vrfy(..., hash_oid=NULL, hash_len=20)
→ compare recovered 20-byte digest to embedded EXPECTED_HASH
Related: VCIH() recomputes SHA-1(client_id) and compares to the same embedded constant (integrity / config-tie check).
| Constant | Value / notes |
|---|---|
| Licensed client id (marker family) | ocrstudio_arafatgroup_trial (also referenced as se_client_id__... in minting tooling) |
| Expected SHA-1 | 25159e611dfa6f5f077a732a01d17ead8cc9770b = SHA-1("ocrstudio_arafatgroup_trial") |
RSA modulus n |
1024-bit (exported as research/ocrstudio_pubkey_*.pem) |
RSA exponent e |
3 |
| Signature format | 256 hex chars → 128 bytes |
Empirical verification of the trial signature shows PKCS#1 v1.5 structure:
EM = 00 01 FF...FF 00 ‖ SHA-1_digest[20]
No DigestInfo. Standard "PKCS#1 v1.5 + SHA-1" verifiers in common libraries (e.g. Python cryptography with DigestInfo) reject these signatures, while a BearSSL-compatible raw-hash unpad accepts them. This is intentional OEM behavior, not a broken trial key.
Vendor-side objects (keypair_generation, sign, activation) indicate:
- Generate RSA-1024 keypair (CRT private form ~ 320 bytes; public ~ 132 bytes)
- Hash client marker → SHA-1
- Embed
CreateStaticAuthData(pub ‖ marker ‖ hash)into the shipped library - Issue customer signature = PKCS#1 sign(hash, priv) as 256 hex chars
- Keep activation/private material secret (640 hex chars if hex-encoded)
Only the holder of the private activation material can mint new valid signatures for a given embedded public key / digest pair.
Artifact: research/verify_static_auth_poc.py
Nature: Independent reimplementation of the verify path only (public operation). No private key, no forge, no binary patch.
| Test | Command / input | Result |
|---|---|---|
Trial signature (from doc/README.md) |
python research/verify_static_auth_poc.py |
PASS (exit 0) |
| Client-id hash self-check | python research/verify_static_auth_poc.py --self-test |
PASS |
| Tampered signature (1 nibble flip) | --signature 3122df27... |
FAIL (exit 1) |
Direct carve of static_auth.cpp.o (1736 bytes, arm64 slice, MH_MAGIC 0xFEEDFACF) from the shipped libocrstudiosdk-ios.a confirms every constant the Python PoC embeds actually lives in the binary __TEXT,__const section at vma 0xe4 (195 bytes):
| Field | Size | PoC embeds | Binary __const |
Match |
|---|---|---|---|---|
RSA modulus n |
128 B | hardcoded hex | offset +0x00 |
✅ identical (byte-for-byte) |
RSA exponent e |
4 B | 00000003 (big-endian) |
offset +0x80 |
✅ identical |
| Client-id marker | var | ocrstudio_arafatgroup_trial |
offset +0x93 inside marker |
✅ present |
EXPECTED_HASH |
20 B | 25159e611dfa6f5f077a732a01d17ead8cc9770b |
offset +0xAF |
✅ identical |
SHA-1(client_id) == EXPECTED_HASH |
— | verified | verified | ✅ |
Reproducer: python check_binary_poc.py (lives in repo as research/check_binary_poc.py) — parses the BSD ar archive, locates the arm64 static_auth.cpp.o slice by MH_MAGIC, parses Mach-O LC_SEGMENT_64, extracts __TEXT,__const, compares all five fields, and prints BINARY CROSS-CHECK: ALL MATCH. Exit 0 only if every field matches.
Also verified negative behaviour of the PoC: empty/null sigs would be skipped by the CBZ x0 at VSA+0x0, malformed sigs (≠256 hex chars) are rejected upstream before BearSSL, and random 256-hex payloads produce bad PKCS#1 header (the tampered-sig test returns 6bb7… at the first 2 bytes — well off the 00 01 required start).
| Path | Description |
|---|---|
research/ocrstudio_pubkey_spki.pem |
Recovered public key (SPKI) |
research/ocrstudio_pubkey_pkcs1.pem |
Recovered public key (PKCS#1) |
research/VENDOR_HARDENING.md |
Detailed mitigation backlog + patch-resistance testing guide |
research/VERIFY_PATH_MAP.md |
Symbol/offset/reloc map of the verify path |
research/check_binary_poc.py |
Carves arm64 static_auth.cpp.o from shipped libocrstudiosdk-ios.a and byte-compares its __const blob against PoC embed constants |
research/VALIDATION_PACKAGE.md |
Full authorization validation + remediation narrative (defers to CLAUDE.md as source of truth) |
research/verification/reference_server_mint.py |
Reference Ed25519 attestation-JWT mint (see § Hardened-Auth Reference Package) |
research/verification/Sources/HardenedAuth/HardenedAuthWrapper.swift |
Reference client wrapper + four-gate verifier |
research/verification/Tests/HardenedAuthTests/OCRAuthHardenedTests.swift |
Reference XCTest suite (§8 matrix) |
research/verification/Package.swift |
SwiftPM entry for swift test / Codemagic |
Samples/Swift/.../OCRStudioSDKSampleViewController.swift |
Trial signature wired for authorized end-to-end session testing |
Described without exploit code. Severity assumes a motivated integrator or attacker with the trial/production xcframework.
Vector: Extract the 256-hex personalized signature from an app binary or source tree and reuse it with the same library build.
Scenario:
An integrator or attacker who has access to a shipped IPA or source repository can locate the signature string passed to CreateSession. Because verification is performed entirely offline, the same string can be copied into another app that embeds the identical ocrstudiosdk.xcframework build. The second app will then pass static auth without any relationship to the original licensee.
Prerequisites:
- Access to a compiled app binary or source that contains the trial/production signature.
- The destination app must embed the same library build (same embedded public key and expected digest).
- No online entitlement check is enforced by the SDK.
Impact:
- License cloning across unrelated apps or organizations for the same library build.
- Trial signatures can be redistributed and used past intended evaluation scope.
- Loss of license inventory control for the OEM.
Observable indicators:
- Identical 256-hex signatures appearing in unrelated app bundles.
- Session success on devices or builds not associated with the licensed customer.
- Absence of any server-side license telemetry or revocation capability.
Mapped mitigations:
- P2 — Operational controls: Prefer server-delivered short-lived tokens over shipping the signature in the binary (see
VENDOR_HARDENING.md§P2). - P1 — Stronger license binding: Sign a structured payload that includes
library_build_id, platform, and validity window so a stolen signature cannot be replayed onto a different product or build (see §7 andVENDOR_HARDENING.md§P1). - P2 — Key separation: Use separate trial and production keypairs, and rotate trial keys per customer build (see
VENDOR_HARDENING.md§P2).
Vector: Modify the static library or linked binary so that the offline authentication check always succeeds, regardless of the supplied signature.
Scenario:
An attacker with the xcframework binary identifies the static-auth entry points in static_auth.cpp.o — se::security::internal::VSA and se::security::internal::VEA. Both are thin wrappers that load the embedded public-key / expected-digest blob from __const and tail-branch into se::security::pkcs1_verify. The attacker could patch any of three conceptual locations: the entry wrappers to skip verification, the compare logic inside pkcs1_verify, or the 20-byte expected digest in the __const blob. Because the companion integrity check VCIH() recomputes SHA-1(client_id) and compares it to the same embedded digest, an attacker who controls both the verify gate and the integrity constant can defeat both checks simultaneously.
Prerequisites:
- Write access to the shipped static library or the linked app binary.
- Ability to locate the auth functions (mangled symbols are present in the archive).
- Understanding that
VSA/VEAtail-branch intopkcs1_verifyand thatEXPECTED_HASHsits adjacent to the public key in the__constblob (seeresearch/VERIFY_PATH_MAP.md).
Impact:
- Complete bypass of offline license validation for the patched binary.
- A single modified library build can be redistributed to disable authentication across all apps that link it.
Observable indicators:
- Modified
libocrstudiosdk-ios.aor app binary with altered bytes in these::security::internal::VSA/pkcs1_verifycode regions. - App succeeds at
CreateSessionwith an empty, malformed, or known-invalid signature. - Unexpected changes to the
__constauth blob near the expected digest offset.
Mapped mitigations:
- P0 — Crypto upgrade: A modern signature scheme raises the cost of any patch-based downgrade, but patching must still be assumed possible (see
VENDOR_HARDENING.md§P0). - P3 — Anti-patch / integrity: Bind
VCIH(or its successor) to a code-region hash of the verify path, not only to the client-id string; diversify checks across multiple locations; avoid a single 20-byte constant compare at a fixed offset (seeVENDOR_HARDENING.md§P3 and § Patch resistance (testing guide) for TR-01…TR-11 test intents). - P2 — Online attestation: For high-value SKUs, supplement offline auth with short-lived server-issued tokens so a patched binary cannot operate indefinitely offline (see
VENDOR_HARDENING.md§P2).
Vector: The static-auth scheme relies on cryptographic primitives that are below current industry standards: RSA-1024, public exponent e = 3, SHA-1, and raw PKCS#1 v1.5 padding without an ASN.1 DigestInfo OID.
Scenario:
The signed payload is the raw 20-byte SHA-1 digest of the client-id marker. BearSSL verifies it with br_rsa_pkcs1_vrfy(..., hash_oid=NULL, hash_len=20), meaning the padding block ends with 00 01 FF...FF 00 ‖ digest rather than the standard PKCS#1 DigestInfo structure. Standard verifiers in common cryptographic libraries therefore reject these signatures, while a raw-hash unpad accepts them. RSA-1024 and e = 3 are historically associated with padding-oracle and Bleichenbacher-class concerns, and SHA-1 no longer provides collision resistance. While forging a signature still requires the vendor private key or a successful cryptographic attack, the overall construction is dated and reduces the margin against future advances.
Prerequisites:
- Cryptanalytic capability or access to the vendor private key (neither was obtained in this assessment).
- Ability to craft a PKCS#1 v1.5 message block that the raw-hash verifier accepts.
Impact:
- A successful cryptographic attack could mint arbitrary valid signatures for the embedded public key.
- Even without a practical forge today, the weak construction accelerates risk as attacks on RSA-1024 and SHA-1 improve.
Observable indicators:
- N/A for a pure cryptographic break; detection requires key rotation and monitoring for signatures that do not match issued license records.
Mapped mitigations:
- P0 — Crypto upgrade: Migrate to RSA-2048+ with public exponent
e = 65537(or Ed25519), use SHA-256 or SHA-512, and replace raw PKCS#1 v1.5 with standard DigestInfo PKCS#1 v1.5 or RSA-PSS (seeVENDOR_HARDENING.md§P0). - P0 — Version the auth blob: Ensure old trial libraries and new keys cannot be mixed, enabling clean cryptographic migration (see
VENDOR_HARDENING.md§P0).
Vector: Signed data is effectively SHA-1(client_id) only.
Impact: License does not cryptographically assert config file identity, library build, platform, or expiry.
Mitigation: Structured signed claims (client, product, platform, build id, config hash, validity window).
Priority order (summary). Full detail: research/VENDOR_HARDENING.md.
| Priority | Action |
|---|---|
| P0 | Upgrade to RSA-2048+ (e=65537) or Ed25519; SHA-256+; RSA-PSS or standard DigestInfo PKCS#1; version the auth blob |
| P1 | Sign structured claims including library_build_id and config_sha256; reject mismatch |
| P2 | Prefer server-issued short-lived tokens for production; separate trial vs production keypairs; rotate trial keys per customer build |
| P3 | Strengthen integrity beyond client-id string hash; assume binary patching will be attempted — test guide: VENDOR_HARDENING.md § Patch resistance (testing guide) |
Per responsible-disclosure practice and tooling policy for this report package:
- No private-key material (none recovered)
Iron Software / OCR Studio may request a private technical workshop under NDA to discuss patch resistance testing methodology without circulating exploit tooling.
- Extract this assessment tree including
research/. - Python 3.10+:
python research/verify_static_auth_poc.py→ expect PASS
python research/verify_static_auth_poc.py --self-test→ expect PASS - Binary cross-check (carves the arm64 static-auth object slice from the shipped
.aand compares its__constblob against every PoC embed):
python research/check_binary_poc.py→ expect BINARY CROSS-CHECK: ALL MATCH (exit 0). - Optional: open
Samples/Swift/OCRStudioSDKSample.xcodeproj, build on device, confirmCreateSessionaccepts the documented trial signature with bundledconfig/*.ocr.
Per authorization letter:
- Findings are confidential to Iron Software / OCR Studio unless written approval is given for third-party disclosure.
- Validity window of assessment authorization: 13 July 2026 – 30 July 2026 (extend in writing if needed).
- No unauthorized redistribution or commercial misuse of Iron Software IP.
Assessor: Adel Noaman — expert.winxp@gmail.com
Vendor contact (from authorization): Daniel Mahony — daniel.mahony@ocrstudio.ai
Envelope ID: E86EA6F7-B675-8E96-8194-18360D9FF672
| Path | Purpose |
|---|---|
CLAUDE.md |
Agent guidance + canonical security disclosure (this file) |
doc/README.md |
API documentation, usage workflows, examples |
OCRStudioSDKCore/include/ocrstudiosdk/ |
C++ header API |
OCRStudioSDKCore/wrap/objcocrstudiosdk/include/ |
Objective-C++ headers |
OCRStudioSDK/ |
Swift/iOS framework layer |
Samples/Swift/OCRStudioSDKSample/ |
Complete working sample app |
OCRStudioSDKCore/lib/ocrstudiosdk.xcframework/ |
Compiled C++ engine binary |
research/VENDOR_HARDENING.md |
OEM mitigations + patch-resistance testing guide |
research/VERIFY_PATH_MAP.md |
Verify-path symbol/offset map |
research/verify_static_auth_poc.py |
Verification-only PoC |
research/check_binary_poc.py |
Binary ↔ PoC constants cross-check (parses libocrstudiosdk-ios.a arm64 slice) |
research/VALIDATION_PACKAGE.md |
Validation + remediation narrative (defers to CLAUDE.md § Hardened-Auth Reference Package) |
research/verification/ |
SwiftPM HardenedAuth + XCTest (§8 / Phase 3) |
research/native_hardened/ |
Native C++/ObjC++ CreateSessionHardened kit + selftest |
OCRStudioSDK/Hardened/OCRStudioSDKHardenedAuth.swift |
High-level JWT gate (wired into initVideoSession) |
SHIP_TO_VENDOR.md |
Vendor handoff — done vs pending, merge steps |
codemagic.yaml |
Codemagic: Swift + native hardened tests |
research/DISCLOSURE_REPORT.md |
Stub → points to this file's disclosure section |
Integrate into new iOS app:
- Link
ocrstudiosdk.xcframeworkin Xcode build phases - Import Objective-C++ headers via bridging header
- Create
OCRStudioSDKInstancewith.ocrconfig file path - Create session with signature and session params JSON
- Call
ProcessImage()withOCRStudioSDKImage - Extract results from
OCRStudioSDKResult
Debug recognition issues:
- Check
Description()methods output (JSON format) for schema details - Verify session params match config file's supported targets
- Enable verbose output modes in session params for additional data
- Monitor
target.IsFinal()to detect incomplete recognition
Enable RFID support:
- Set
__RFID__preprocessor flag - Add NFCPassportReader SPM dependency
- Configure Info.plist with required NFC identifiers
- Add "Near Field Communication Tag Reading" capability in Xcode