
Tamper-proof state machine and verifiable multi-step ledger protecting checkouts, onboarding, KYC and server-driven flows from step-skipping, parameter tampering, and replay using signed deltas and strict hash lineage.
Tamper-Proof State Machine & Verifiable Multi-Step Flow Ledger for Kotlin Multiplatform (KMP).
Protect checkout flows, onboarding wizards, KYC funnels, and server-driven navigation from step skipping, parameter tampering, and replay attacks — across Android, iOS, Desktop, and Server.
HTTPS encrypts traffic between client and server, but it does not protect your business logic from a compromised client. On rooted devices, modified APKs, or via simple deep-link manipulation, attackers can:
TrustChain solves this at the architectural level. It models your navigation and state transitions as a cryptographically verifiable chain (similar to a lightweight blockchain ledger). Each step is strictly tied to the cryptographic digest of the previous step, and all state mutations are digitally signed by the server. If a step is skipped or tampered with, the client-side state machine fails fast before any action is executed.
| Security Concern | Standard REST + HTTPS | JWT Tokens | TrustChain |
|---|---|---|---|
| Data In-Transit Encryption | Yes (TLS) | Yes (TLS) | Yes (TLS) |
| Prevent Step Skipping (e.g. Deep Links) | No (Requires complex backend logic) | No | Yes (Automatic Parent Hash Check) |
| Detect Tampered State Deltas | No | Partially | Yes (Asymmetric Signature Verification) |
| Replay Attack Prevention | Manual nonces required | No | Yes (Strict Hash Lineage) |
| Zero Runtime Dependencies | N/A | Often heavy | Yes (Pure Kotlin Multiplatform) |
graph LR
subgraph Server [SERVER]
ServerLogic[Business Logic<br>Workflow Step] --> HashCalc[Compute State Hash<br>Link previousHash]
ServerLogic --> Signer[Sign Delta Updates<br>Private Key]
HashCalc --> ResponsePayload[Blocks / Deltas<br>JSON / ProtoBuf]
Signer --> ResponsePayload
end
subgraph Client [CLIENT]
Transport[Transport Layer] --> EventDispatcher[Chain Event<br>Push / Pop / Patch]
EventDispatcher --> Reducer[TrustChain Reducer<br>Pure State Machine]
subgraph Verification [Zero-Trust Engine]
Reducer --> HashVerify{Parent Hash<br>Matches?}
HashVerify -- Yes --> SigVerify{Signature<br>Valid?}
SigVerify -- Yes --> Patcher[Block Patcher<br>Apply & Re-hash]
end
Patcher --> NewChain[New Immutable<br>TrustChain Stack]
end
ResponsePayload -->|Network Transport| Transport
style Server fill: #e8daef, stroke: #6c3483, color: #1a1a1a
style Client fill: #d6eaf8, stroke: #2471a3, color: #1a1a1a
style Verification fill: #d5f5e3, stroke: #1e8449, color: #1a1a1a
style ResponsePayload fill: #f9e79f, stroke: #b7950b, color: #1a1a1a
style Transport fill: #aed6f1, stroke: #1f618d, color: #1a1a1a
linkStyle default stroke: #333, stroke-width: 2px
linkStyle 3 stroke: #c0392b, stroke-width: 3pxThe server establishes the chain of authority by referencing the client's current hash in subsequent blocks and signing
state mutations. The client maintains an immutable TrustChain stack that validates all hashes and cryptographic
signatures before committing state changes.
| Module | Target | Description |
|---|---|---|
:library:core |
Common / KMP | Pure functional state machine, interfaces, events, and validation logic without third-party runtime dependencies. |
:example |
JVM / CLI | Standalone runnable demonstration featuring a full multi-step checkout workflow and simulated MITM/skip-step attack. |
Any step, screen, or transaction in your domain implements VerifiableBlock:
interface VerifiableBlock {
val id: String
val hash: String
val previousHash: String?
}hash — Deterministic digest of the block's content and state.previousHash — Hash of the preceding block in the stack (null for the root/genesis block).Instead of resending full screen models or state trees for minor updates, the server produces a DeltaBlock:
@Serializable
data class DeltaBlock<P>(
val patches: P,
val previousHash: String,
val newHash: String,
val signature: String = ""
)patches — Arbitrary patch payload (e.g., Map<String, String>, JSON object, or typed delta).previousHash — The exact block hash this patch must be applied to.newHash — The expected resulting block hash after the patch is applied.signature — Base64-encoded digital signature (e.g., RSA, Ed25519) of newHash.Mutations to the state machine are dispatched as pure algebraic data types:
sealed interface ChainEvent<out B : VerifiableBlock, out P> {
data class Push<B : VerifiableBlock>(val block: B) : ChainEvent<B, Nothing>
data object Pop : ChainEvent<Nothing, Nothing>
data class Replace<B : VerifiableBlock>(val block: B) : ChainEvent<B, Nothing>
data class ApplyDeltas<P>(val blocks: List<DeltaBlock<P>>) : ChainEvent<Nothing, P>
}The TrustChain container evaluates events immutably via reduce():
val result: Result<TrustChain<MyBlock, MyPatch>> = chain.reduce(ChainEvent.Push(nextBlock))Every successful transition records its action type:
enum class ChainAction {
PUSH, POP, REPLACE, PATCH, IDLE
}When validation fails in strictMode, reduction halts immediately with an explicit typed error:
sealed class ChainError(message: String) : RuntimeException(message) {
data class HashMismatch(val expected: String?, val actual: String?) : ChainError(...)
data class SignatureVerificationFailed(val blockHash: String) : ChainError(...)
class CannotPopEmptyChain : ChainError(...)
data class UnsupportedDeltaOperation(val detail: String) : ChainError(...)
}TrustChain delegates asymmetric verification to a pluggable interface:
fun interface SignatureVerifier {
fun verify(payload: String, signatureBase64: String): Boolean
}When applying deltas, a BlockPatcher defines how the current block incorporates the patch:
fun interface BlockPatcher<B : P VerifiableBlock, in > {
fun patch(block: B, patches: P, newHash: String): B
}When advancing the user flow, the server references the user's active session hash:
fun proceedToShipping(sessionId: String, address: String): CheckoutStep {
val currentHash = sessionStore.getHash(sessionId) ?: error("Session expired")
val content = "prev=$currentHash;step=shipping;addr=$address"
val newHash = Sha256.digest(content)
sessionStore.updateHash(sessionId, newHash)
return CheckoutStep(
id = "step_shipping",
title = "Shipping & Address",
hash = newHash,
previousHash = currentHash
)
}For partial updates (e.g., applying a promotional discount or updating live status):
fun applyPromo(sessionId: String, promoCode: String): DeltaBlock<Map<String, String>> {
val currentHash = sessionStore.getHash(sessionId)
val patches = mapOf("discount" to "10%", "total" to "$180")
val newHash = Sha256.digest("$currentHash;promo=$promoCode")
val signature = ServerKeyStore.sign(newHash) // RSA or Ed25519
sessionStore.updateHash(sessionId, newHash)
return DeltaBlock(
patches = patches,
previousHash = currentHash,
newHash = newHash,
signature = signature
)
}Configure the client chain container with strict verification rules:
var chain = TrustChain<CheckoutStep, Map<String, String>>(
strictMode = true,
verifier = SignatureVerifier { payload, signatureBase64 ->
// Verify with server's public key (RSA / Ed25519)
CryptoEngine.verifySignature(publicKey, payload, signatureBase64)
},
patcher = { currentStep, patches, newHash ->
currentStep.copy(
state = currentStep.state + patches,
hash = newHash
)
}
)// 1. Push next screen
chain = chain.reduce(ChainEvent.Push(step1)).getOrThrow()
// 2. Apply signed deltas received from WebSocket / HTTP
val delta = receiveDeltaFromServer()
chain = chain.reduce(ChainEvent.ApplyDeltas(listOf(delta))).getOrThrow()
// 3. Pop back
if (chain.canPop) {
chain = chain.reduce(ChainEvent.Pop).getOrThrow()
}Atomic reductions ensure that failures do not corrupt the active state stack:
chain.reduce(ChainEvent.Push(untrustedBlock)).fold(
onSuccess = { updatedChain ->
chain = updatedChain
},
onFailure = { error ->
when (error) {
is ChainError.HashMismatch -> {
logger.error("Tamper Alert: Hash link broken! Expected ${error.expected}, got ${error.actual}")
}
is ChainError.SignatureVerificationFailed -> {
logger.error("Security Alert: Server signature invalid for hash ${error.blockHash}")
}
else -> logger.error("Chain error: ${error.message}")
}
}
)A complete executable workflow simulation is included in the :example module:
example/
└── src/main/kotlin/io/github/numq/trustchain/example/
├── CheckoutStep.kt # VerifiableBlock domain implementation
├── CheckoutServer.kt # In-memory server tracking session hashes
├── SimpleCrypto.kt # Digest & signature verification logic
└── Application.kt # CLI runner demonstrating valid & MITM attack flows
Run directly via Gradle:
./gradlew :example:runOutput:
======================================================
TrustChain Zero-Trust Flow Demo
======================================================
[+] 1. Cart loaded:
Step: Shopping Cart | Hash: h_c2c1a28598b8ad69
[+] 2. Shipping confirmed:
Step: Shipping & Address | Hash: h_50bda3ba68cced0d | Prev: h_c2c1a28598b8ad69
[+] 3. Signed promo patch applied:
Discount: 10% | New Total: 1890$
New Hash: h_6471c5f0c53a644b
[+] 4. Payment reached:
Step: Payment Authorization | Hash: h_6a6fd155e392f7ef
Legitimate flow completed. Stack size: 3
------------------------------------------------------
Simulating Attack: Attacker tries to skip shipping
------------------------------------------------------
Attacker has Cart loaded.
Attacker attempts to directly apply Payment step (bypassing Shipping)...
[!] BLOCKED BY TRUSTCHAIN: State machine integrity preserved.
Error: Hash mismatch: expected 'h_c2c1a28598b8ad69', actual 'h_6471c5f0c53a644b'
Add the dependency to your Kotlin Multiplatform build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.numq.trustchain:core:1.0.0")
}
}
}data class Step(
override val id: String,
override val hash: String,
override val previousHash: String? = null
) : VerifiableBlock
// Setup chain
val root = Step(id = "1", hash = "root_hash", previousHash = null)
val next = Step(id = "2", hash = "step_2_hash", previousHash = "root_hash")
var chain = TrustChain<Step, Unit>(strictMode = true)
chain = chain.reduce(ChainEvent.Push(root)).getOrThrow()
chain = chain.reduce(ChainEvent.Push(next)).getOrThrow()
println("Current active step: ${chain.current?.id}") // Prints: 2Apache-2.0 License - see LICENSE file for details.
Support Development: numq.github.io/support
Tamper-Proof State Machine & Verifiable Multi-Step Flow Ledger for Kotlin Multiplatform (KMP).
Protect checkout flows, onboarding wizards, KYC funnels, and server-driven navigation from step skipping, parameter tampering, and replay attacks — across Android, iOS, Desktop, and Server.
HTTPS encrypts traffic between client and server, but it does not protect your business logic from a compromised client. On rooted devices, modified APKs, or via simple deep-link manipulation, attackers can:
TrustChain solves this at the architectural level. It models your navigation and state transitions as a cryptographically verifiable chain (similar to a lightweight blockchain ledger). Each step is strictly tied to the cryptographic digest of the previous step, and all state mutations are digitally signed by the server. If a step is skipped or tampered with, the client-side state machine fails fast before any action is executed.
| Security Concern | Standard REST + HTTPS | JWT Tokens | TrustChain |
|---|---|---|---|
| Data In-Transit Encryption | Yes (TLS) | Yes (TLS) | Yes (TLS) |
| Prevent Step Skipping (e.g. Deep Links) | No (Requires complex backend logic) | No | Yes (Automatic Parent Hash Check) |
| Detect Tampered State Deltas | No | Partially | Yes (Asymmetric Signature Verification) |
| Replay Attack Prevention | Manual nonces required | No | Yes (Strict Hash Lineage) |
| Zero Runtime Dependencies | N/A | Often heavy | Yes (Pure Kotlin Multiplatform) |
graph LR
subgraph Server [SERVER]
ServerLogic[Business Logic<br>Workflow Step] --> HashCalc[Compute State Hash<br>Link previousHash]
ServerLogic --> Signer[Sign Delta Updates<br>Private Key]
HashCalc --> ResponsePayload[Blocks / Deltas<br>JSON / ProtoBuf]
Signer --> ResponsePayload
end
subgraph Client [CLIENT]
Transport[Transport Layer] --> EventDispatcher[Chain Event<br>Push / Pop / Patch]
EventDispatcher --> Reducer[TrustChain Reducer<br>Pure State Machine]
subgraph Verification [Zero-Trust Engine]
Reducer --> HashVerify{Parent Hash<br>Matches?}
HashVerify -- Yes --> SigVerify{Signature<br>Valid?}
SigVerify -- Yes --> Patcher[Block Patcher<br>Apply & Re-hash]
end
Patcher --> NewChain[New Immutable<br>TrustChain Stack]
end
ResponsePayload -->|Network Transport| Transport
style Server fill: #e8daef, stroke: #6c3483, color: #1a1a1a
style Client fill: #d6eaf8, stroke: #2471a3, color: #1a1a1a
style Verification fill: #d5f5e3, stroke: #1e8449, color: #1a1a1a
style ResponsePayload fill: #f9e79f, stroke: #b7950b, color: #1a1a1a
style Transport fill: #aed6f1, stroke: #1f618d, color: #1a1a1a
linkStyle default stroke: #333, stroke-width: 2px
linkStyle 3 stroke: #c0392b, stroke-width: 3pxThe server establishes the chain of authority by referencing the client's current hash in subsequent blocks and signing
state mutations. The client maintains an immutable TrustChain stack that validates all hashes and cryptographic
signatures before committing state changes.
| Module | Target | Description |
|---|---|---|
:library:core |
Common / KMP | Pure functional state machine, interfaces, events, and validation logic without third-party runtime dependencies. |
:example |
JVM / CLI | Standalone runnable demonstration featuring a full multi-step checkout workflow and simulated MITM/skip-step attack. |
Any step, screen, or transaction in your domain implements VerifiableBlock:
interface VerifiableBlock {
val id: String
val hash: String
val previousHash: String?
}hash — Deterministic digest of the block's content and state.previousHash — Hash of the preceding block in the stack (null for the root/genesis block).Instead of resending full screen models or state trees for minor updates, the server produces a DeltaBlock:
@Serializable
data class DeltaBlock<P>(
val patches: P,
val previousHash: String,
val newHash: String,
val signature: String = ""
)patches — Arbitrary patch payload (e.g., Map<String, String>, JSON object, or typed delta).previousHash — The exact block hash this patch must be applied to.newHash — The expected resulting block hash after the patch is applied.signature — Base64-encoded digital signature (e.g., RSA, Ed25519) of newHash.Mutations to the state machine are dispatched as pure algebraic data types:
sealed interface ChainEvent<out B : VerifiableBlock, out P> {
data class Push<B : VerifiableBlock>(val block: B) : ChainEvent<B, Nothing>
data object Pop : ChainEvent<Nothing, Nothing>
data class Replace<B : VerifiableBlock>(val block: B) : ChainEvent<B, Nothing>
data class ApplyDeltas<P>(val blocks: List<DeltaBlock<P>>) : ChainEvent<Nothing, P>
}The TrustChain container evaluates events immutably via reduce():
val result: Result<TrustChain<MyBlock, MyPatch>> = chain.reduce(ChainEvent.Push(nextBlock))Every successful transition records its action type:
enum class ChainAction {
PUSH, POP, REPLACE, PATCH, IDLE
}When validation fails in strictMode, reduction halts immediately with an explicit typed error:
sealed class ChainError(message: String) : RuntimeException(message) {
data class HashMismatch(val expected: String?, val actual: String?) : ChainError(...)
data class SignatureVerificationFailed(val blockHash: String) : ChainError(...)
class CannotPopEmptyChain : ChainError(...)
data class UnsupportedDeltaOperation(val detail: String) : ChainError(...)
}TrustChain delegates asymmetric verification to a pluggable interface:
fun interface SignatureVerifier {
fun verify(payload: String, signatureBase64: String): Boolean
}When applying deltas, a BlockPatcher defines how the current block incorporates the patch:
fun interface BlockPatcher<B : P VerifiableBlock, in > {
fun patch(block: B, patches: P, newHash: String): B
}When advancing the user flow, the server references the user's active session hash:
fun proceedToShipping(sessionId: String, address: String): CheckoutStep {
val currentHash = sessionStore.getHash(sessionId) ?: error("Session expired")
val content = "prev=$currentHash;step=shipping;addr=$address"
val newHash = Sha256.digest(content)
sessionStore.updateHash(sessionId, newHash)
return CheckoutStep(
id = "step_shipping",
title = "Shipping & Address",
hash = newHash,
previousHash = currentHash
)
}For partial updates (e.g., applying a promotional discount or updating live status):
fun applyPromo(sessionId: String, promoCode: String): DeltaBlock<Map<String, String>> {
val currentHash = sessionStore.getHash(sessionId)
val patches = mapOf("discount" to "10%", "total" to "$180")
val newHash = Sha256.digest("$currentHash;promo=$promoCode")
val signature = ServerKeyStore.sign(newHash) // RSA or Ed25519
sessionStore.updateHash(sessionId, newHash)
return DeltaBlock(
patches = patches,
previousHash = currentHash,
newHash = newHash,
signature = signature
)
}Configure the client chain container with strict verification rules:
var chain = TrustChain<CheckoutStep, Map<String, String>>(
strictMode = true,
verifier = SignatureVerifier { payload, signatureBase64 ->
// Verify with server's public key (RSA / Ed25519)
CryptoEngine.verifySignature(publicKey, payload, signatureBase64)
},
patcher = { currentStep, patches, newHash ->
currentStep.copy(
state = currentStep.state + patches,
hash = newHash
)
}
)// 1. Push next screen
chain = chain.reduce(ChainEvent.Push(step1)).getOrThrow()
// 2. Apply signed deltas received from WebSocket / HTTP
val delta = receiveDeltaFromServer()
chain = chain.reduce(ChainEvent.ApplyDeltas(listOf(delta))).getOrThrow()
// 3. Pop back
if (chain.canPop) {
chain = chain.reduce(ChainEvent.Pop).getOrThrow()
}Atomic reductions ensure that failures do not corrupt the active state stack:
chain.reduce(ChainEvent.Push(untrustedBlock)).fold(
onSuccess = { updatedChain ->
chain = updatedChain
},
onFailure = { error ->
when (error) {
is ChainError.HashMismatch -> {
logger.error("Tamper Alert: Hash link broken! Expected ${error.expected}, got ${error.actual}")
}
is ChainError.SignatureVerificationFailed -> {
logger.error("Security Alert: Server signature invalid for hash ${error.blockHash}")
}
else -> logger.error("Chain error: ${error.message}")
}
}
)A complete executable workflow simulation is included in the :example module:
example/
└── src/main/kotlin/io/github/numq/trustchain/example/
├── CheckoutStep.kt # VerifiableBlock domain implementation
├── CheckoutServer.kt # In-memory server tracking session hashes
├── SimpleCrypto.kt # Digest & signature verification logic
└── Application.kt # CLI runner demonstrating valid & MITM attack flows
Run directly via Gradle:
./gradlew :example:runOutput:
======================================================
TrustChain Zero-Trust Flow Demo
======================================================
[+] 1. Cart loaded:
Step: Shopping Cart | Hash: h_c2c1a28598b8ad69
[+] 2. Shipping confirmed:
Step: Shipping & Address | Hash: h_50bda3ba68cced0d | Prev: h_c2c1a28598b8ad69
[+] 3. Signed promo patch applied:
Discount: 10% | New Total: 1890$
New Hash: h_6471c5f0c53a644b
[+] 4. Payment reached:
Step: Payment Authorization | Hash: h_6a6fd155e392f7ef
Legitimate flow completed. Stack size: 3
------------------------------------------------------
Simulating Attack: Attacker tries to skip shipping
------------------------------------------------------
Attacker has Cart loaded.
Attacker attempts to directly apply Payment step (bypassing Shipping)...
[!] BLOCKED BY TRUSTCHAIN: State machine integrity preserved.
Error: Hash mismatch: expected 'h_c2c1a28598b8ad69', actual 'h_6471c5f0c53a644b'
Add the dependency to your Kotlin Multiplatform build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.numq.trustchain:core:1.0.0")
}
}
}data class Step(
override val id: String,
override val hash: String,
override val previousHash: String? = null
) : VerifiableBlock
// Setup chain
val root = Step(id = "1", hash = "root_hash", previousHash = null)
val next = Step(id = "2", hash = "step_2_hash", previousHash = "root_hash")
var chain = TrustChain<Step, Unit>(strictMode = true)
chain = chain.reduce(ChainEvent.Push(root)).getOrThrow()
chain = chain.reduce(ChainEvent.Push(next)).getOrThrow()
println("Current active step: ${chain.current?.id}") // Prints: 2Apache-2.0 License - see LICENSE file for details.
Support Development: numq.github.io/support