A security-first research prototype that enforces IoT data authenticity, confidentiality, integrity, and replay resistance by anchoring cryptographic proofs on a local Ethereum blockchain (Ganache) and storing raw payloads on IPFS — all exposed through a production-style FastAPI backend.
Built to demonstrate applied cryptography and distributed-systems security design for a cybersecurity engineering context.
- Problem Statement
- What Was Implemented
- Architecture & System Design
- Security Controls
- Prerequisites & Installation
- Usage / Quick Start
- Testing
- Configuration / Environment Variables
- Future Work
- License
IoT ecosystems suffer from a structural trust problem: sensor data travels through multiple untrusted hops (device → network → server), and at any point it can be forged, tampered with, replayed, or silently dropped without detection.
This project tackles four concrete attack surfaces:
| Threat | Attack Vector |
|---|---|
| Forged data | Any actor impersonating a registered device |
| Payload tampering | Man-in-the-middle modification in transit or at rest |
| Replay attacks | Re-submitting a valid, previously captured packet |
| Unauthorized data access | No provenance or verifiability after ingestion |
flowchart LR
A[IoT Device --> Encrypt + Sign] -->|Encrypted payload + nonce + timestamp| B
B{FastAPI Backend\nValidate}
B -->|Reject| R([401 Unauthorized])
B -->|Pass| C[Decrypt & Hash SHA-256]
C -->|Plaintext JSON| D[(IPFS)]
C -->|CID + Hash + Address| E[(DataStorage.sol + Ethereum)]
E -.->|verifyDataIntegrity| F([Integrity Check --> true / false])
Smart Contract — contracts/DataStorage.sol
- Solidity
^0.8.20with OpenZeppelinOwnableandReentrancyGuard - Stores per-device records: IPFS CID, SHA-256 data hash, device address, data type, block timestamp
- Functions:
registerDevice(owner-only),storeDataHash,verifyDataIntegrity,grantAccess/revokeAccess, enumeration helpers - Deployed to
localhost(Ganache) and Polygon Amoy testnet
FastAPI Backend — app/
| Endpoint | Method | Purpose |
|---|---|---|
/api/iot/register |
POST |
Enroll device with ECDSA public key; registers on-chain |
/api/iot/upload |
POST |
Accept encrypted + signed sensor payload; full validation pipeline |
/api/iot/verify/integrity |
POST |
Recompute hash and validate against chain record |
/api/iot/devices/{id}/status |
GET |
Device registration status |
/api/iot/devices/{id}/uploads |
GET |
Paginated upload history |
/api/iot/onchain/records |
GET |
Recent on-chain data records |
IoT Device Simulator — iot_device/device_simulator.py
- Generates mock sensor data (temperature, humidity, etc.)
- Derives Ethereum-style device address from ECDSA secp256k1 public key (keccak, last 20 bytes)
- Encrypts payload with AES-256-GCM, signs canonical upload envelope
Cryptographic Services — app/services/
encryption.py— AES-256-GCM encrypt/decrypt with canonical JSON serializationsignature.py— canonical JSON bytes, SHA-256 digest, ECDSA secp256k1 signature verificationregistry.py— in-memory device registry with nonce cache and timestamp freshness check
| Layer | Technology | Role |
|---|---|---|
| Smart Contract | Solidity 0.8.20, OpenZeppelin | Immutable on-chain data anchoring |
| Blockchain | Ganache (local), Polygon Amoy (testnet) | EVM execution environment |
| Web3 Interface | Web3.py | Python ↔ EVM bridge |
| Backend API | FastAPI, Uvicorn, Pydantic v2 | Secure HTTP API with request validation |
| Decentralized Storage | IPFS (ipfshttpclient) |
Content-addressed plaintext storage |
| Payload Crypto | cryptography (AES-256-GCM) |
Authenticated encryption |
| Identity Crypto | ecdsa (secp256k1) |
Device authentication and non-repudiation |
| Contract Tooling | py-solc-x, npm @openzeppelin/contracts |
Compilation and deployment |
| Testing | Pytest, FastAPI TestClient |
Automated security regression suite |
- Device sends
POST /registerwithdevice_id(IOT-...prefix enforced) and ECDSA public key - Backend derives Ethereum checksum address from public key; calls
registerDeviceon-chain - Device constructs upload: encrypts sensor payload (AES-256-GCM), signs canonical envelope (ECDSA), attaches timestamp and nonce
- Backend validates: registered device → timestamp freshness (configurable
REPLAY_WINDOW_SECONDS) → nonce uniqueness → ECDSA signature - Backend decrypts payload, serializes to canonical JSON, computes SHA-256
- Backend uploads plaintext to IPFS; receives content CID
- Backend calls
storeDataHash(cid, dataType, deviceAddress, dataHash)on-chain; returns IPFS CID + tx hash
| Decision | Rationale | Trade-off |
|---|---|---|
| Canonical JSON before hashing/signing | Eliminates key-ordering ambiguity across languages | Slight serialization overhead |
| AES-256-GCM (authenticated encryption) | Provides both confidentiality AND integrity in one primitive | Requires secure key distribution |
| ECDSA secp256k1 (same curve as Ethereum) | Enables on-chain address derivation from public key without extra key types | Requires careful DER/raw format handling |
| In-memory nonce cache | Simple, fast replay prevention for prototype | Non-persistent — cleared on restart |
| IPFS for raw payload, blockchain for hash only | Keeps transaction costs low; IPFS provides content-addressing | IPFS availability is not guaranteed (no pinning in prototype) |
| Localhost-first (Ganache) | Zero-cost deterministic testing environment | Not production; requires explicit deploy step |
Each control maps to a concrete code path and an automated test:
| Control | Implementation | Automated Test |
|---|---|---|
| Device authentication | ECDSA secp256k1 signature verification on every upload | test_upload_rejected_on_invalid_signature |
| Payload confidentiality | AES-256-GCM encryption end-to-end | test_encryption.py — round-trip |
| Payload integrity (crypto) | AES-GCM authentication tag; tampered ciphertext raises InvalidTag |
test_encrypt_then_tamper_decrypt_raises |
| Replay prevention | Nonce uniqueness cache + timestamp freshness window | test_upload_rejected_on_replay_nonce |
| Immutable audit trail | SHA-256 hash anchored on blockchain; verifyDataIntegrity call |
test_verify_integrity_true_and_false |
| Device authorization | Registration check before any upload is processed | test_upload_rejected_when_unregistered |
| Structured security logging | All rejection paths emit structured log events | Backend log output |
| Startup validation | Missing/malformed env vars raise immediately at launch | app/config.py — get_settings() |
Latest test run: 9 passed, 4 warnings
| Dependency | Minimum Version | Purpose |
|---|---|---|
| Python | 3.10+ | Backend and device simulator |
| Node.js + npm | 18.x | OpenZeppelin contract dependencies |
| Ganache CLI | Latest | Local EVM (or Docker equivalent) |
| IPFS Kubo | Latest | Local IPFS node |
| Docker Desktop | Optional | Run IPFS as container |
1. Clone the repository
git clone https://github.com/menotliam/Intergrating-Blockchain-In-IOT.git
cd Intergrating-Blockchain-In-IOT2. Create and activate a Python virtual environment
python -m venv myenv
myenv\Scripts\Activate.ps13. Install Python dependencies
pip install -r requirement.txt4. Install Solidity/OpenZeppelin dependencies
npm install
cd contracts && npm install && cd ..5. Start local blockchain & IPFS
npx ganache --host 127.0.0.1 --port 8545 --chain.chainId 1337 --wallet.totalAccounts 5 # Terminal 1st
# Copy Account #0 address, private key --> ACCOUNT_ADDRESS, PRIVATE_KEY (without 0x)
ipfs daemon # Terminal 2nd6. Configure environment variables
cp .env.example .env
# Generate AES key:
python -c "import os,base64; print(base64.b64encode(os.urandom(32)).decode())"
# Edit .env — replace CONTRACT_ADDRESS, PRIVATE_KEY, and DEVICE_SHARED_AES_KEY7. Compile and deploy the smart contract
python contracts/compile.py
python contracts/deploy.py localhost
# Copy the printed CONTRACT_ADDRESS into .envcurl -X POST http://localhost:8000/api/iot/register \
-H "Content-Type: application/json" \
-d '{
"device_id": "IOT-DEMO001",
"public_key": "<hex-encoded-ecdsa-secp256k1-public-key>"
}'Expected response:
{
"status": "registered",
"device_id": "IOT-DEMO001",
"device_address": "0xAbCd...",
"tx_hash": "0x1a2b..."
}curl -X POST http://localhost:8000/api/iot/upload \
-H "Content-Type: application/json" \
-d '{
"device_id": "IOT-DEMO001",
"encrypted_payload": { "ciphertext": "...", "nonce": "...", "tag": "..." },
"timestamp": 1712600000,
"nonce": "unique-uuid-v4",
"signature": "<hex-ecdsa-signature>"
}'Expected response:
{
"status": "stored",
"ipfs_cid": "QmXyz...",
"tx_hash": "0x3c4d...",
"data_hash": "sha256:abcd..."
}curl -X POST http://localhost:8000/api/iot/verify/integrity \
-H "Content-Type: application/json" \
-d '{
"device_id": "IOT-DEMO001",
"plain_data": { "temperature": 22.5, "humidity": 60 },
"record_index": 0
}'Expected response:
{
"integrity_ok": true,
"chain_hash": "sha256:abcd...",
"computed_hash": "sha256:abcd..."
}# Initialize device keys and register
python iot_device/device_simulator.py init_deviceTests use Pytest with a FastAPI TestClient. Blockchain and IPFS calls are replaced with test doubles so the full security logic runs without external infrastructure.
.\scripts\run_tests.ps1Or directly:
$env:PYTEST_DISABLE_PLUGIN_AUTOLOAD='1'; python -m pytest -q tests| Test File | What It Validates |
|---|---|
tests/test_encryption.py |
AES-256-GCM round-trip; tampered ciphertext raises InvalidTag |
tests/test_signature.py |
Valid ECDSA verification passes; tampered payload fails |
tests/test_iot_routes.py |
Full route security: register, upload success, replay rejection, invalid signature rejection, unregistered device rejection, integrity endpoint true/false, paginated on-chain records |
| Scenario | Expected HTTP Status | Test Name |
|---|---|---|
| Valid register + upload | 200 |
test_upload_success_after_registration |
| Unregistered device upload | 401 |
test_upload_rejected_when_unregistered |
| Invalid ECDSA signature | 401 |
test_upload_rejected_on_invalid_signature |
| Replayed nonce | 401 |
test_upload_rejected_on_replay_nonce |
| Integrity check — match | integrity_ok: true |
test_verify_integrity_true_and_false |
| Integrity check — tampered | integrity_ok: false |
test_verify_integrity_true_and_false |
Copy .env.example to .env and populate each value. Never commit .env to source control.
| Variable | Type | Default / Example | Description |
|---|---|---|---|
APP_HOST |
string |
0.0.0.0 |
FastAPI bind address |
APP_PORT |
int |
8000 |
FastAPI listen port |
BLOCKCHAIN_RPC_URL |
string |
http://127.0.0.1:8545 |
JSON-RPC endpoint (Ganache or testnet) |
CONTRACT_ADDRESS |
string |
0x0000... |
Deployed DataStorage.sol address |
ACCOUNT_ADDRESS |
string |
0xdB44... |
Ethereum account used to send transactions |
PRIVATE_KEY |
string |
(no default) | Hex private key for transaction signing (no 0x prefix) |
CONTRACT_ABI_PATH |
string |
contracts/build/artifacts/DataStorage.json |
Path to compiled ABI file |
IPFS_API_URL |
string |
/dns/localhost/tcp/5001/http |
IPFS multiaddr API endpoint |
DEVICE_SHARED_AES_KEY |
string |
(no default) | Base64-encoded 32-byte AES-256-GCM key |
REPLAY_WINDOW_SECONDS |
int |
300 |
Maximum age of an accepted upload timestamp (seconds) |
DEVICE_ID |
string |
IOT-DEMO001 |
Simulator device identifier (must start with IOT-) |
PRIVATE_KEY_PATH |
string |
iot_device/device_private_key.pem |
Path to device ECDSA private key PEM file |
BACKEND_REGISTER_URL |
string |
http://localhost:8000/api/iot/register |
Simulator registration endpoint |
BACKEND_UPLOAD_URL |
string |
http://localhost:8000/api/iot/upload |
Simulator upload endpoint |
Security note:
PRIVATE_KEYandDEVICE_SHARED_AES_KEYare secrets. Rotate them before any non-local use. The.env.examplefile contains placeholder values only — they are not production keys.
This is an intentional prototype. The following areas are defined for production hardening:
| Area | Gap in Prototype | Proposed Direction |
|---|---|---|
| Persistence | Device registry and nonce cache are in-memory only | Add PostgreSQL/Redis with encrypted-at-rest storage |
| API Authentication | No operator-level auth on backend endpoints | Implement JWT or mTLS for API consumers |
| Key Management | Shared AES key via env var; no lifecycle management | Integrate AWS KMS, HashiCorp Vault, or TPM-backed HSM |
| Contract Modularity | Single DataStorage.sol handles all concerns |
Split into Registry, AuditLog, and AccessControl contracts |
| Threat-Driven Testing | Happy-path and rejection tests only | Add DoS simulation, key-compromise scenarios, and fault injection |
| Testnet / Mainnet Deployment | Localhost only; Amoy deploy exists but untested at scale | Model gas costs, finality, and key custody for a target testnet |
| IPFS Durability | No pinning or replication | Integrate Pinata or Filecoin for guaranteed persistence |
| Observability | Structured logging only | Add Prometheus metrics, distributed tracing, and alerting |
Ngo Giang
Aspiring Cybersecurity & Network Engineer
This project is released under the MIT License.
Prototype built for cybersecurity research and learning. Not intended for production deployment without addressing the future-work items above.