APIPricingDocsBlogPartnersContact
Back to blog
Tutorial

Securing Biometric Data in Transit: Encryption, Signatures, and Compliance

How to protect biometric attendance data from device to cloud to your application β€” with TLS, HMAC signatures, and compliance-ready architecture.

PunchConnect TeamΒ·Mar 28, 2026Β·5 min read

Introduction

A fingerprint template leaks. Unlike a password, you can't reset it. Your employees' biometric data is irrevocable β€” and that makes securing it non-negotiable.

Biometric attendance data includes fingerprint templates, face recognition vectors, employee IDs, and timestamps. In most jurisdictions, this qualifies as sensitive personal data. GDPR (EU), LGPD (Brazil), POPIA (South Africa), PDPA (Thailand) β€” they all demand encryption in transit, access controls, and audit trails.

This guide covers the full security chain: device to cloud, cloud to your application, and everything in between.

The Threat Model

Before we talk solutions, let's understand what we're defending against. Biometric attendance systems face three primary attack surfaces:

<svg viewBox="0 0 700 300" xmlns="http://www.w3.org/2000/svg" style="max-width:700px;width:100%;font-family:system-ui,sans-serif">
<rect width="700" height="300" fill="#1a1a2e" rx="12"/>
<text x="350" y="30" fill="#e0e0e0" font-size="17" font-weight="bold" text-anchor="middle">Biometric Data β€” Three Attack Surfaces</text>
<!-- Surface 1: Device to Cloud -->
<rect x="30" y="55" width="195" height="120" rx="8" fill="#3e1e1e" stroke="#ef5350" stroke-width="1"/>
<text x="127" y="80" fill="#ef5350" font-size="15" font-weight="bold" text-anchor="middle">β‘  Device β†’ Cloud</text>
<text x="127" y="105" fill="#ccc" font-size="14" text-anchor="middle">Man-in-the-middle</text>
<text x="127" y="125" fill="#ccc" font-size="14" text-anchor="middle">Packet sniffing</text>
<text x="127" y="145" fill="#ccc" font-size="14" text-anchor="middle">DNS spoofing</text>
<text x="127" y="165" fill="#81c784" font-size="14" text-anchor="middle">Fix: TLS 1.3</text>
<!-- Surface 2: Cloud to App -->
<rect x="252" y="55" width="195" height="120" rx="8" fill="#3e1e1e" stroke="#ef5350" stroke-width="1"/>
<text x="350" y="80" fill="#ef5350" font-size="15" font-weight="bold" text-anchor="middle">β‘‘ Cloud β†’ Your App</text>
<text x="350" y="105" fill="#ccc" font-size="14" text-anchor="middle">Fake webhook injection</text>
<text x="350" y="125" fill="#ccc" font-size="14" text-anchor="middle">Replay attacks</text>
<text x="350" y="145" fill="#ccc" font-size="14" text-anchor="middle">Endpoint hijacking</text>
<text x="350" y="165" fill="#81c784" font-size="14" text-anchor="middle">Fix: HMAC-SHA256</text>
<!-- Surface 3: At Rest -->
<rect x="475" y="55" width="195" height="120" rx="8" fill="#3e1e1e" stroke="#ef5350" stroke-width="1"/>
<text x="572" y="80" fill="#ef5350" font-size="15" font-weight="bold" text-anchor="middle">β‘’ Data at Rest</text>
<text x="572" y="105" fill="#ccc" font-size="14" text-anchor="middle">Database breach</text>
<text x="572" y="125" fill="#ccc" font-size="14" text-anchor="middle">Backup theft</text>
<text x="572" y="145" fill="#ccc" font-size="14" text-anchor="middle">Insider access</text>
<text x="572" y="165" fill="#81c784" font-size="14" text-anchor="middle">Fix: AES-256</text>
<!-- Bottom note -->
<rect x="30" y="200" width="640" height="80" rx="8" fill="#1b3a2d" stroke="#81c784" stroke-width="1"/>
<text x="350" y="225" fill="#81c784" font-size="15" font-weight="bold" text-anchor="middle">PunchConnect Security Chain</text>
<text x="350" y="250" fill="#ccc" font-size="14" text-anchor="middle">TLS 1.3 (in transit) β†’ HMAC-SHA256 (webhook integrity) β†’ AES-256 (at rest)</text>
<text x="350" y="270" fill="#ccc" font-size="14" text-anchor="middle">All layers enforced by default β€” no configuration needed</text>
</svg>

Layer 1: Device-to-Cloud Encryption (TLS 1.3)

All communication between biometric devices and PunchConnect is encrypted using TLS 1.3 β€” the latest transport layer security protocol. This means:

- No plaintext data ever leaves the device over the network
- Certificate pinning prevents man-in-the-middle attacks
- Forward secrecy ensures that even if a key is compromised, past sessions remain encrypted

You don't need to configure this. When you register a device in the PunchConnect dashboard, the encrypted connection is established automatically. The device connects outbound on port 443 (HTTPS) β€” no special firewall rules, no VPN, no static IP required.

What Gets Encrypted

Every piece of data that moves between a device and PunchConnect is encrypted:

- Attendance records (employee ID, timestamp, verification type)
- Employee templates (fingerprint, face, card data)
- Device commands (restart, sync, configuration updates)
- Device status reports (online/offline, firmware version)

Layer 2: Webhook Signature Verification (HMAC-SHA256)

When PunchConnect delivers attendance data to your application via webhook, how do you know it's really from PunchConnect? An attacker who discovers your webhook endpoint could send fake attendance events.

The answer: every webhook includes a cryptographic signature.

<svg viewBox="0 0 700 260" xmlns="http://www.w3.org/2000/svg" style="max-width:700px;width:100%;font-family:system-ui,sans-serif">
<rect width="700" height="260" fill="#1a1a2e" rx="12"/>
<text x="350" y="30" fill="#e0e0e0" font-size="17" font-weight="bold" text-anchor="middle">Webhook Signature Verification Flow</text>
<!-- PunchConnect box -->
<rect x="30" y="60" width="180" height="80" rx="8" fill="#1a3a5c" stroke="#4fc3f7" stroke-width="2"/>
<text x="120" y="90" fill="#4fc3f7" font-size="14" font-weight="bold" text-anchor="middle">PunchConnect</text>
<text x="120" y="110" fill="#ccc" font-size="14" text-anchor="middle">Signs payload with</text>
<text x="120" y="128" fill="#ccc" font-size="14" text-anchor="middle">your webhook secret</text>
<!-- Arrow -->
<line x1="210" y1="100" x2="310" y2="100" stroke="#4fc3f7" stroke-width="2" marker-end="url(#arr-sec)"/>
<text x="260" y="85" fill="#81c784" font-size="14" text-anchor="middle">HTTPS</text>
<defs><marker id="arr-sec" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#4fc3f7"/></marker></defs>
<!-- Payload -->
<rect x="310" y="50" width="170" height="100" rx="8" fill="#2d2d44" stroke="#444" stroke-width="1"/>
<text x="395" y="75" fill="#e0e0e0" font-size="14" font-weight="bold" text-anchor="middle">HTTP POST</text>
<text x="395" y="97" fill="#ccc" font-size="12" text-anchor="middle">X-PunchConnect-Signature:</text>
<text x="395" y="115" fill="#ff9800" font-size="12" text-anchor="middle">hmac_sha256(secret, body)</text>
<text x="395" y="137" fill="#ccc" font-size="12" text-anchor="middle">Body: {"event":"punch"...}</text>
<!-- Arrow -->
<line x1="480" y1="100" x2="530" y2="100" stroke="#4fc3f7" stroke-width="2" marker-end="url(#arr-sec)"/>
<!-- Your app -->
<rect x="530" y="60" width="140" height="80" rx="8" fill="#1b3a2d" stroke="#81c784" stroke-width="2"/>
<text x="600" y="90" fill="#81c784" font-size="14" font-weight="bold" text-anchor="middle">Your App</text>
<text x="600" y="110" fill="#ccc" font-size="14" text-anchor="middle">Verifies signature</text>
<text x="600" y="128" fill="#ccc" font-size="14" text-anchor="middle">before processing</text>
<!-- Bottom: what happens -->
<rect x="30" y="175" width="310" height="65" rx="8" fill="#1b3a2d" stroke="#81c784" stroke-width="1"/>
<text x="185" y="198" fill="#81c784" font-size="14" font-weight="bold" text-anchor="middle">βœ… Signature matches</text>
<text x="185" y="220" fill="#ccc" font-size="14" text-anchor="middle">Process attendance data</text>
<rect x="360" y="175" width="310" height="65" rx="8" fill="#3e1e1e" stroke="#ef5350" stroke-width="1"/>
<text x="515" y="198" fill="#ef5350" font-size="14" font-weight="bold" text-anchor="middle">❌ Signature mismatch</text>
<text x="515" y="220" fill="#ccc" font-size="14" text-anchor="middle">Reject β†’ log β†’ alert</text>
</svg>

Python: Verify Webhook Signature

python
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_signing_secret"
def verify_signature(payload: bytes, signature: str) -> bool:
"""Verify PunchConnect webhook signature."""
expected = hmac.new(
WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route("/webhooks/attendance", methods=["POST"])
def handle_attendance():
signature = request.headers.get("X-PunchConnect-Signature", "")
if not verify_signature(request.data, signature):
# Log the attempt β€” this could be an attack
app.logger.warning(
f"Invalid webhook signature from {request.remote_addr}"
)
return jsonify({"error": "Invalid signature"}), 401
event = request.json
# Safe to process β€” signature verified
employee_id = event["data"]["employee_id"]
timestamp = event["data"]["timestamp"]
device_id = event["data"]["device_id"]
print(f"Verified punch: {employee_id} at {timestamp} on {device_id}")
return jsonify({"received": True}), 200
if __name__ == "__main__":
app.run(port=3000, ssl_context="adhoc") # HTTPS required

Node.js: Verify Webhook Signature

Critical: Always use hmac.compare_digest (Python) or crypto.timingSafeEqual (Node.js) instead of ===. Simple string comparison is vulnerable to timing attacks.

javascript
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));
const WEBHOOK_SECRET = process.env.PUNCHCONNECT_WEBHOOK_SECRET;
function verifySignature(payload, signature) {
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post('/webhooks/attendance', (req, res) => {
const signature = req.headers['x-punchconnect-signature'];
if (!signature || !verifySignature(req.rawBody, signature)) {
console.warn(`⚠️ Invalid signature from ${req.ip}`);
return res.status(401).json({ error: 'Invalid signature' });
}
const { data } = req.body;
console.log(`βœ… Verified: ${data.employee_id} at ${data.timestamp}`);
res.status(200).json({ received: true });
});
app.listen(3000, () => console.log('Secure webhook listener on :3000'));

Layer 3: Data at Rest (AES-256)

All data stored in PunchConnect's infrastructure is encrypted using AES-256:

- Attendance records
- Employee biometric templates
- Device configurations
- Webhook delivery logs
- Database backups

Access to encrypted data requires rotating credentials managed by automated systems. No human has direct database access in production.

HTTPS-Only Webhook Delivery

PunchConnect will not deliver webhooks to plain HTTP endpoints in production mode. Your webhook URL must use HTTPS with a valid TLS certificate.

For local development, use a tunneling service like ngrok:

```bash
# Start a secure tunnel for local testing
ngrok http 3000

# Use the HTTPS URL from ngrok
# https://abc123.ngrok.io/webhooks/attendance
```

bash
# βœ… This works
curl -X POST https://api.punchconnect.com/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/attendance",
"events": ["attendance.punch"],
"secret": "whsec_your_signing_secret"
}'
# ❌ This will be rejected
# "url": "http://your-app.com/webhooks/attendance"
# Error: "Webhook URL must use HTTPS"

Replay Attack Prevention

Even with valid signatures, an attacker could capture a legitimate webhook and replay it. Protect against this with timestamp validation and idempotency checks:

python
from datetime import datetime, timezone, timedelta
MAX_WEBHOOK_AGE = timedelta(minutes=5)
@app.route("/webhooks/attendance", methods=["POST"])
def handle_attendance():
# Step 1: Verify signature (from above)
if not verify_signature(request.data, request.headers.get("X-PunchConnect-Signature", "")):
return jsonify({"error": "Invalid signature"}), 401
event = request.json
# Step 2: Check timestamp freshness
event_time = datetime.fromisoformat(event["timestamp"])
age = datetime.now(timezone.utc) - event_time
if age > MAX_WEBHOOK_AGE:
app.logger.warning(f"Stale webhook rejected: {age} old")
return jsonify({"error": "Webhook too old"}), 400
# Step 3: Idempotency β€” reject duplicate event IDs
event_id = event["event_id"]
if redis_client.exists(f"webhook:{event_id}"):
return jsonify({"status": "already_processed"}), 200
redis_client.setex(f"webhook:{event_id}", 86400, "1") # TTL: 24h
# Step 4: Process the event
process_attendance(event["data"])
return jsonify({"received": True}), 200

API Key Security Best Practices

Your PunchConnect API key is the gateway to your attendance data. Treat it like a database password.

Key rotation checklist:

1. Generate a new API key in the PunchConnect dashboard
2. Update your application's environment variables
3. Deploy with the new key
4. Verify webhooks are still being delivered
5. Revoke the old key
6. Log the rotation date

python
# ❌ NEVER do this
API_KEY = "pk_live_abc123" # Hardcoded in source
# βœ… Load from environment
import os
API_KEY = os.environ["PUNCHCONNECT_API_KEY"]
# βœ… Or from a secrets manager
from your_secrets_lib import get_secret
API_KEY = get_secret("punchconnect/api-key")

Compliance Checklist by Regulation

| Requirement | GDPR (EU) | LGPD (Brazil) | POPIA (S. Africa) | PunchConnect |
|-------------|-----------|---------------|-------------------|--------------|
| Encryption in transit | Required | Required | Required | βœ… TLS 1.3 |
| Encryption at rest | Required | Required | Required | βœ… AES-256 |
| Access controls | Required | Required | Required | βœ… API keys + RBAC |
| Audit trail | Required | Required | Required | βœ… Webhook logs |
| Data minimization | Required | Required | Required | βœ… Templates only, no raw biometrics |
| Right to erasure | Article 17 | Article 18 | Section 24 | βœ… DELETE /employees/{id} |
| Breach notification | 72 hours | "Reasonable time" | "As soon as possible" | βœ… Status page + email alerts |

Delete Employee Data (Right to Erasure)

This removes:
- Employee record
- All biometric templates (fingerprint, face, card)
- Attendance history association
- Device-stored templates (pushed as delete command to all devices)

bash
# GDPR Article 17 / LGPD Article 18 β€” Right to erasure
curl -X DELETE https://api.punchconnect.com/v1/employees/EMP001 \
-H "Authorization: Bearer YOUR_API_KEY"

Security Audit Checklist

Use this checklist when deploying PunchConnect in a security-sensitive environment:

- [ ] Webhook URL uses HTTPS with valid certificate
- [ ] Webhook signature verification implemented (HMAC-SHA256)
- [ ] Timestamp validation rejects webhooks older than 5 minutes
- [ ] Idempotency handling prevents duplicate event processing
- [ ] API keys stored in environment variables or secrets manager
- [ ] API key rotation scheduled (every 90 days recommended)
- [ ] Network access restricted β€” webhook endpoint only accepts PunchConnect IPs
- [ ] Logging captures all webhook deliveries and signature failures
- [ ] Employee deletion API tested for right-to-erasure compliance
- [ ] Data retention policy defined and automated

FAQ

Does PunchConnect store raw fingerprint images?

No. PunchConnect only stores fingerprint templates β€” mathematical representations of fingerprint features, not actual images. Templates cannot be reverse-engineered into fingerprint images. This is a critical distinction for GDPR and LGPD compliance.

What TLS version does PunchConnect use?

All connections use TLS 1.3 (the latest version). TLS 1.0 and 1.1 are not supported. TLS 1.2 is accepted as a fallback for older devices, but TLS 1.3 is negotiated by default.

How do I verify that my webhook endpoint is secure?

PunchConnect provides a webhook testing tool in the dashboard. Send a test event and verify: (1) your server receives it over HTTPS, (2) signature verification passes, and (3) you can reject events with tampered signatures. You can also test with cURL by sending a POST with an invalid signature to confirm your server rejects it.

Can I restrict API access to specific IP addresses?

Yes. In the PunchConnect dashboard, you can configure IP allowlists for your API keys. Only requests from whitelisted IPs will be accepted. This adds a network-level security layer on top of API key authentication.

What happens to data if I cancel my PunchConnect account?

All data is deleted within 30 days of account cancellation. You can export your data via the API before cancellation. After deletion, data cannot be recovered β€” this is by design, to comply with data minimization requirements.

---

Biometric data security isn't optional β€” it's a legal requirement in most markets. PunchConnect handles the heavy lifting: TLS 1.3 for transit, AES-256 for storage, HMAC-SHA256 for webhook integrity. Your job is to verify webhook signatures and store API keys securely. Everything else is handled.

Start your free 7-day trial β€” enterprise-grade security from day one, no credit card required.

Related articles

Securing Biometric Data in Transit: Encryption, Signatures, and Compliance | PunchConnect