Verify a receipt server-side before serving content
If you're delivering AI-generated artifacts (PDFs, reports, contracts) that consumers trust because they're signed, you need to verify the signature on every fetch — not just on creation. Otherwise a tampered copy will reach the client.
When to use this pattern
- — Distributing signed AI-generated PDFs (audit reports, legal opinions, medical summaries) where a client downloads and expects to verify
- — Serving AI content via CDN where the path between origin and edge could be compromised
- — Public APIs that return signed AI artifacts — verification at fetch is what makes "designed for FRE 902(13)/(14) admissibility" defensible end-to-end
- — Multi-tenant systems where one tenant's compromised content shouldn't reach another tenant
Pattern: middleware verification
Wrap your content-serving route with a verification check. Reject (or warn loudly) before sending bytes to the client.
import { NextRequest, NextResponse } from 'next/server'
import { CertNode } from '@certnode/sdk'
const cert = new CertNode({ apiKey: process.env.CERTNODE_API_KEY! })
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ artifactId: string }> }
) {
const { artifactId } = await params
// 1. Look up the artifact + its signing receipt
const artifact = await db.artifacts.findUnique({
where: { id: artifactId },
select: { content: true, receiptId: true },
})
if (!artifact) {
return NextResponse.json({ error: 'not_found' }, { status: 404 })
}
// 2. Verify the receipt
const verification = await cert.verify({ receiptId: artifact.receiptId })
if (!verification.valid) {
// Receipt invalid — refuse to serve
return NextResponse.json(
{ error: 'receipt_invalid', detail: 'Refusing to serve unsigned or tampered content' },
{ status: 403 }
)
}
// 3. Re-hash the stored content, compare to the receipt's contentHash
// (catches database/CDN tampering — receipt may verify cryptographically
// but the stored content has drifted from what was signed)
const currentContentHash = await sha256(artifact.content)
if (currentContentHash !== verification.receipt!.contentHash) {
// Stored content doesn't match what was signed — refuse + alert
await alertOps('content_drift', { artifactId, receiptId: artifact.receiptId })
return NextResponse.json(
{ error: 'content_drift', detail: 'Stored content has been modified since signing' },
{ status: 403 }
)
}
// 4. Verification passed — serve with receipt info in response headers
return new NextResponse(artifact.content, {
headers: {
'Content-Type': 'application/pdf',
'X-CertNode-Receipt-Id': artifact.receiptId,
'X-CertNode-Verify-Url': verification.receipt!.verifyUrl,
'X-CertNode-Signed-At': verification.receipt!.signedAt,
},
})
}
async function sha256(content: string | Buffer): Promise<string> {
const crypto = await import('crypto')
return crypto.createHash('sha256').update(content).digest('hex')
}The two checks matter
- 1. Receipt-validity check catches a tampered receipt — someone modified the timestamp, swapped signing keys, or fabricated a receipt that doesn't exist on CertNode.
- 2. Content-hash check catches drift between what was signed and what you're about to serve. The receipt may be perfectly valid but for content that no longer matches what's in your database — common when an attacker has DB access but no signing key.
Both checks must pass. Either failure means refuse to serve.
Performance — caching verification
Verification adds an outbound call per request. For high-traffic content:
// Cache verification result — receipts don't change once signed
import { unstable_cache as cache } from 'next/cache'
const verifyReceiptCached = cache(
async (receiptId: string) => cert.verify({ receiptId }),
['cert-verify'],
{ revalidate: 3600, tags: ['cert-receipt'] } // 1-hour cache
)
// In the route handler
const verification = await verifyReceiptCached(artifact.receiptId)Cache only the verification result, not the content-hash check. Content drift can happen between cache writes; always re-hash the current content.
What the client sees
The response headers expose the receipt info so client-side code can show a "Verified ✓" badge or link to the public verify page:
const response = await fetch('/api/artifacts/abc123')
const receiptId = response.headers.get('X-CertNode-Receipt-Id')
const verifyUrl = response.headers.get('X-CertNode-Verify-Url')
// UI: show verified badge with link
<a href={verifyUrl}>
✓ Cryptographically verified · Receipt {receiptId.slice(0, 8)}…
</a>