ENTERPRISE / DATA STREAM
Verify every delivery.
SentinelFeed signs Enterprise Data Stream POSTs with Ed25519. Your receiver can authenticate the sender, detect changes to the body, and reject old or repeated deliveries before trusting the JSON.
Production signing identity
- Key ID
sf-ed25519-v1- Public key · raw Ed25519, base64
teEskjes9wFF06PkLWaxrSEbHJimNZ5jp2N7zWb7Qw4=- SHA-256 of the decoded 32-byte public key
sha256:3e53c9f279b608978521694456010db11108fa1652e86bc245206cf017c88745
Stable files: public key and fingerprint. Their full URLs are:
https://getsentinelfeed.com/keys/sf-ed25519-v1-public-key.txthttps://getsentinelfeed.com/keys/sf-ed25519-v1-fingerprint.txt
Pin the fingerprint in your receiver after obtaining it through a trusted channel. Check that the decoded public key hashes to that fingerprint. The Key ID header is a lookup hint, not proof of identity; never take a replacement key from an incoming request.
Version 1 wire protocol
Transport uses HTTPS. The body is UTF-8 JSON. SentinelFeed serializes it once and sends the exact bytes it signed. Read the raw HTTP request bytes before parsing or reserializing JSON.
| Header | Expected value |
|---|---|
Content-Type | application/json |
X-SentinelFeed-Key-Id | sf-ed25519-v1 |
X-SentinelFeed-Event | daily-vulnerability-brief |
X-SentinelFeed-Timestamp | Unix seconds, base-10 ASCII |
X-SentinelFeed-Delivery-Id | Lowercase canonical UUID v4 |
X-SentinelFeed-Signature | v1= + standard padded base64 of the 64-byte Ed25519 signature |
Exactly these bytes are signed with Ed25519:
ASCII(timestamp) + "." + ASCII(delivery_id) + "." + raw_body
The period separators are single 0x2e bytes. Accept a timestamp only within ±5 minutes of your clock. Verify the signature first, then atomically record the Delivery ID for at least 10 minutes; reject a repeated ID even if its signature is valid. Keep clocks synchronized. A transient retry has the same timestamp, ID, body and signature. A new logical delivery gets a new ID. Across receiver instances use shared atomic TTL storage, such as Redis SET key value NX EX 600. A retry outside the five-minute window must be rejected.
Python example
Install cryptography. Pass the framework's raw request bytes, such as Flask's request.get_data() before get_json(). The in-memory replay store below illustrates the check; use an atomic shared store in production.
import base64
import binascii
import hashlib
import json
import time
import uuid
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
public_raw = base64.b64decode('teEskjes9wFF06PkLWaxrSEbHJimNZ5jp2N7zWb7Qw4=', validate=True)
if len(public_raw) != 32 or "sha256:" + hashlib.sha256(public_raw).hexdigest() != 'sha256:3e53c9f279b608978521694456010db11108fa1652e86bc245206cf017c88745':
raise ValueError("untrusted public key")
public_key = Ed25519PublicKey.from_public_bytes(public_raw)
seen_ids = {} # example only: delivery_id -> expiry, one process
def verify_delivery(raw_body: bytes, headers):
if not isinstance(raw_body, bytes):
raise ValueError("raw request bytes required")
if headers.get("X-SentinelFeed-Key-Id") != "sf-ed25519-v1":
raise ValueError("unknown signing key")
if headers.get("X-SentinelFeed-Event") != "daily-vulnerability-brief":
raise ValueError("unexpected event")
if headers.get("Content-Type") != "application/json":
raise ValueError("unexpected content type")
timestamp = headers.get("X-SentinelFeed-Timestamp", "")
delivery_id = headers.get("X-SentinelFeed-Delivery-Id", "")
signature_header = headers.get("X-SentinelFeed-Signature", "")
if not (timestamp.isascii() and timestamp.isdecimal() and len(timestamp) == 10):
raise ValueError("invalid timestamp")
now = time.time()
if abs(now - int(timestamp)) > 300:
raise ValueError("stale or future delivery")
try:
if str(uuid.UUID(delivery_id)) != delivery_id or uuid.UUID(delivery_id).version != 4:
raise ValueError("invalid UUID")
except (ValueError, AttributeError) as exc:
raise ValueError("invalid delivery ID") from exc
if not signature_header.startswith("v1="):
raise ValueError("unknown signature version")
try:
signature = base64.b64decode(signature_header[3:], validate=True)
except binascii.Error as exc:
raise ValueError("invalid signature encoding") from exc
if len(signature) != 64 or base64.b64encode(signature).decode("ascii") != signature_header[3:]:
raise ValueError("invalid signature encoding")
message = timestamp.encode("ascii") + b"." + delivery_id.encode("ascii") + b"." + raw_body
try:
public_key.verify(signature, message)
except InvalidSignature as exc:
raise ValueError("signature mismatch") from exc
if seen_ids.get(delivery_id, 0) > now:
raise ValueError("replayed delivery")
seen_ids[delivery_id] = now + 600 # atomic SET NX with TTL in production
return json.loads(raw_body) # only now trust/parse the JSON
Node.js example
Pass the original request Buffer before JSON middleware parses it. Node's Ed25519 verifier uses the standard SPKI wrapper for the published raw key. Replace the illustrative Map with a shared atomic TTL store in production.
const { createHash, createPublicKey, verify } = require('node:crypto');
const publicRaw = Buffer.from('teEskjes9wFF06PkLWaxrSEbHJimNZ5jp2N7zWb7Qw4=', 'base64');
if (publicRaw.length !== 32 || publicRaw.toString('base64') !== 'teEskjes9wFF06PkLWaxrSEbHJimNZ5jp2N7zWb7Qw4=') throw Error('invalid public key');
if ('sha256:' + createHash('sha256').update(publicRaw).digest('hex') !== 'sha256:3e53c9f279b608978521694456010db11108fa1652e86bc245206cf017c88745') throw Error('untrusted key');
const publicKey = createPublicKey({
key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), publicRaw]),
format: 'der', type: 'spki',
});
const seenIds = new Map(); // example only; use atomic shared TTL storage in production
function verifyDelivery(rawBody, headers) {
if (!Buffer.isBuffer(rawBody)) throw Error('raw request Buffer required');
if (headers['x-sentinelfeed-key-id'] !== 'sf-ed25519-v1' ||
headers['x-sentinelfeed-event'] !== 'daily-vulnerability-brief' ||
headers['content-type'] !== 'application/json') throw Error('unexpected headers');
const timestamp = headers['x-sentinelfeed-timestamp'];
const deliveryId = headers['x-sentinelfeed-delivery-id'];
const signatureHeader = headers['x-sentinelfeed-signature'];
if (typeof timestamp !== 'string' || !/^\d{10}$/.test(timestamp) ||
Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) throw Error('stale delivery');
if (typeof deliveryId !== 'string' ||
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(deliveryId)) throw Error('invalid ID');
if (typeof signatureHeader !== 'string' || !signatureHeader.startsWith('v1=')) throw Error('unknown signature');
const encoded = signatureHeader.slice(3);
const signature = Buffer.from(encoded, 'base64');
if (signature.length !== 64 || signature.toString('base64') !== encoded) throw Error('invalid signature encoding');
const message = Buffer.concat([Buffer.from(`${timestamp}.${deliveryId}.`, 'ascii'), rawBody]);
if (!verify(null, message, publicKey, signature)) throw Error('invalid signature');
const now = Date.now();
if ((seenIds.get(deliveryId) || 0) > now) throw Error('replayed delivery');
seenIds.set(deliveryId, now + 600_000); // atomic SET NX + expiry in production
return JSON.parse(rawBody.toString('utf8')); // parse only after authentication
}
