NIST Randomness Beacon Signature Verification
This guide documents how to cryptographically verify pulses from the NIST Randomness Beacon 2.0. The verification process is not well-documented by NIST, so we're sharing our implementation for others.
Overview
The NIST Randomness Beacon generates 512-bit random values every 60 seconds using quantum random number generators. Each pulse is:
- RSA-signed using X.509 certificates
- Hash-chained where
outputValue = SHA-512(signature_input + signatureValue)
We verify both properties to ensure pulses are authentic.
Why This Matters
Without verification, a compromised network path could:
- Replay old pulses as if they were new
- Forge pulses with predictable values
- Manipulate randomness used in downstream applications
By verifying RSA signatures against NIST's published certificates, we ensure each pulse is authentic.
The Verification Challenge
NIST publishes documentation about their beacon, but the exact binary format for signature verification is not clearly specified. Through reverse engineering and testing against real API responses, we determined the correct format.
Key insight: The signature input requires length-prefixed fields in a specific binary format.
Two-Step Verification
Step 1: RSA Signature Verification
The signature is RSA with PKCS#1 v1.5 padding and SHA-512 (cipher suite 0).
RSA_Verify(
public_key = certificate.public_key,
signature = signatureValue (hex-decoded),
message = signature_input (binary)
)
Step 2: Hash Chain Verification
The output value is derived from the signature input and signature:
outputValue = SHA-512(signature_input + signatureValue)
Where signature_input is the same binary message used in Step 1.
The Signature Input Format
This is the critical part that's not well-documented. Each field is encoded with a 4-byte big-endian length prefix.
Field Order
The signature input concatenates these fields in order:
| Field | Encoding |
|---|---|
| uri | length(4B) + UTF-8 bytes |
| version | length(4B) + UTF-8 bytes |
| cipherSuite | 4B big-endian integer |
| period | 4B big-endian integer |
| certificateId | length(4B) + hex-decoded bytes |
| chainIndex | 8B big-endian integer |
| pulseIndex | 8B big-endian integer |
| timeStamp | length(4B) + UTF-8 bytes |
| localRandomValue | length(4B) + hex-decoded bytes |
| external.sourceId | length(4B) + hex-decoded bytes (if present) |
| external.statusCode | 4B big-endian integer (if present) |
| external.value | length(4B) + hex-decoded bytes (if present) |
| listValues[].value | length(4B) + hex-decoded bytes (for each) |
| precommitmentValue | length(4B) + hex-decoded bytes (if present) |
| statusCode | 4B big-endian integer (if present) |
Python Implementation
def build_signature_input(pulse_data: dict) -> bytes:
"""
Build the signature input message from pulse fields.
Each string field is prefixed with strlen() as 4-byte big-endian.
Each hex field is hex-decoded and prefixed with length() as 4-byte big-endian.
Integer fields are encoded directly as big-endian bytes.
"""
parts: list[bytes] = []
# strlen(uri) + uri as UTF-8
uri_bytes = pulse_data["uri"].encode("utf-8")
parts.append(len(uri_bytes).to_bytes(4, byteorder="big"))
parts.append(uri_bytes)
# strlen(version) + version as UTF-8
version_bytes = pulse_data["version"].encode("utf-8")
parts.append(len(version_bytes).to_bytes(4, byteorder="big"))
parts.append(version_bytes)
# cipherSuite as 4-byte big-endian
parts.append(int(pulse_data["cipherSuite"]).to_bytes(4, byteorder="big"))
# period as 4-byte big-endian
parts.append(int(pulse_data["period"]).to_bytes(4, byteorder="big"))
# length(certificateId) + certificateId as hex-decoded bytes
cert_bytes = bytes.fromhex(pulse_data["certificateId"])
parts.append(len(cert_bytes).to_bytes(4, byteorder="big"))
parts.append(cert_bytes)
# chainIndex as 8-byte big-endian
parts.append(int(pulse_data["chainIndex"]).to_bytes(8, byteorder="big"))
# pulseIndex as 8-byte big-endian
parts.append(int(pulse_data["pulseIndex"]).to_bytes(8, byteorder="big"))
# strlen(timestamp) + timestamp as UTF-8
ts_bytes = pulse_data["timeStamp"].encode("utf-8")
parts.append(len(ts_bytes).to_bytes(4, byteorder="big"))
parts.append(ts_bytes)
# length(localRandomValue) + localRandomValue as hex-decoded bytes
lrv_bytes = bytes.fromhex(pulse_data["localRandomValue"])
parts.append(len(lrv_bytes).to_bytes(4, byteorder="big"))
parts.append(lrv_bytes)
# External source fields (if present)
external = pulse_data.get("external", {})
if external:
# length(sourceId) + sourceId as hex-decoded bytes
src_bytes = bytes.fromhex(external["sourceId"])
parts.append(len(src_bytes).to_bytes(4, byteorder="big"))
parts.append(src_bytes)
# external/statusCode as 4-byte big-endian
parts.append(int(external["statusCode"]).to_bytes(4, byteorder="big"))
# length(value) + value as hex-decoded bytes
val_bytes = bytes.fromhex(external["value"])
parts.append(len(val_bytes).to_bytes(4, byteorder="big"))
parts.append(val_bytes)
# listValues (in order provided)
for lv in pulse_data.get("listValues", []):
lv_bytes = bytes.fromhex(lv["value"])
parts.append(len(lv_bytes).to_bytes(4, byteorder="big"))
parts.append(lv_bytes)
# length(precommitmentValue) + precommitmentValue as hex-decoded bytes
if "precommitmentValue" in pulse_data:
pc_bytes = bytes.fromhex(pulse_data["precommitmentValue"])
parts.append(len(pc_bytes).to_bytes(4, byteorder="big"))
parts.append(pc_bytes)
# statusCode as 4-byte big-endian
if "statusCode" in pulse_data:
parts.append(int(pulse_data["statusCode"]).to_bytes(4, byteorder="big"))
return b"".join(parts)
Complete Verification Example
#!/usr/bin/env python3
"""
Verify a NIST Beacon pulse.
Usage: python verify_nist.py
"""
import hashlib
import httpx
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
NIST_BEACON_URL = "https://beacon.nist.gov/beacon/2.0"
def fetch_certificate(certificate_id: str) -> x509.Certificate:
"""Fetch X.509 certificate from NIST."""
url = f"{NIST_BEACON_URL}/certificate/{certificate_id}"
response = httpx.get(url, timeout=10.0)
response.raise_for_status()
return x509.load_pem_x509_certificate(response.text.encode())
def verify_nist_pulse(pulse_data: dict) -> str:
"""
Verify a NIST beacon pulse.
Returns the output value on success, raises on failure.
"""
signature_value = pulse_data["signatureValue"]
output_value = pulse_data["outputValue"]
# Build the signature input message
signature_input = build_signature_input(pulse_data)
# Step 1: Verify RSA signature
certificate = fetch_certificate(pulse_data["certificateId"])
public_key = certificate.public_key()
signature_bytes = bytes.fromhex(signature_value)
public_key.verify(
signature_bytes,
signature_input,
padding.PKCS1v15(),
hashes.SHA512(),
)
print(f"RSA signature: VALID")
# Step 2: Verify hash chain
combined = signature_input + signature_bytes
computed_hash = hashlib.sha512(combined).hexdigest().upper()
if computed_hash != output_value.upper():
raise ValueError(
f"Hash chain failed: computed {computed_hash[:32]}..., "
f"expected {output_value[:32]}..."
)
print(f"Hash chain: VALID")
return output_value.lower()
def main():
# Fetch the latest pulse
response = httpx.get(f"{NIST_BEACON_URL}/pulse/last", timeout=10.0)
response.raise_for_status()
pulse = response.json()["pulse"]
print(f"Pulse Index: {pulse['pulseIndex']}")
print(f"Timestamp: {pulse['timeStamp']}")
result = verify_nist_pulse(pulse)
print(f"Output Value: {result[:32]}...")
print(f"\nVerification: SUCCESS")
if __name__ == "__main__":
main()
Fetching Certificates
NIST publishes X.509 certificates for each signing key:
GET https://beacon.nist.gov/beacon/2.0/certificate/{certificateId}
The response is a PEM-encoded X.509 certificate. Certificates are signed by commercial CAs (like DigiCert), so you can optionally verify the certificate chain.
Tip: Cache certificates by certificateId to avoid repeated fetches. NIST uses the same certificate for many pulses.
Common Pitfalls
1. Wrong Field Order
The signature input must follow the exact field order specified above. Reordering fields produces an invalid signature.
2. Missing Length Prefixes
String and hex fields require 4-byte big-endian length prefixes. Without them, verification fails.
3. Hex vs UTF-8 Encoding
certificateId,localRandomValue,signatureValue,outputValueare hex strings that must be decoded to bytesuri,version,timeStampare UTF-8 strings
4. Integer Sizes
chainIndexandpulseIndexare 8-byte integerscipherSuite,period, andstatusCodeare 4-byte integers
5. Optional Fields
Some fields like external, listValues, precommitmentValue, and statusCode are optional. Only include them in the signature input if they exist in the pulse data.
API Reference
Fetch Latest Pulse
curl https://beacon.nist.gov/beacon/2.0/pulse/last
Fetch Specific Pulse
curl https://beacon.nist.gov/beacon/2.0/chain/1/pulse/1000000
Pulse Response Structure
{
"pulse": {
"uri": "https://beacon.nist.gov/beacon/2.0/chain/2/pulse/1709730",
"version": "2.0",
"cipherSuite": 0,
"period": 60000,
"certificateId": "04c5b3b2...",
"chainIndex": 2,
"pulseIndex": 1709730,
"timeStamp": "2026-03-21T16:43:00.000Z",
"localRandomValue": "8a7b3c2d...",
"external": null,
"listValues": [],
"precommitmentValue": "f1e2d3c4...",
"statusCode": 0,
"signatureValue": "3a4b5c6d...",
"outputValue": "9f8e7d6c..."
}
}
References
See Also
- Verification Guide - Verify rng.dev rounds
- How It Works - Technical details of generation
- Threat Model - Security considerations