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:

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.

HeaderExpected value
Content-Typeapplication/json
X-SentinelFeed-Key-Idsf-ed25519-v1
X-SentinelFeed-Eventdaily-vulnerability-brief
X-SentinelFeed-TimestampUnix seconds, base-10 ASCII
X-SentinelFeed-Delivery-IdLowercase canonical UUID v4
X-SentinelFeed-Signaturev1= + 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
}