Skip to main content

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:

  1. RSA-signed using X.509 certificates
  2. 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:

FieldEncoding
urilength(4B) + UTF-8 bytes
versionlength(4B) + UTF-8 bytes
cipherSuite4B big-endian integer
period4B big-endian integer
certificateIdlength(4B) + hex-decoded bytes
chainIndex8B big-endian integer
pulseIndex8B big-endian integer
timeStamplength(4B) + UTF-8 bytes
localRandomValuelength(4B) + hex-decoded bytes
external.sourceIdlength(4B) + hex-decoded bytes (if present)
external.statusCode4B big-endian integer (if present)
external.valuelength(4B) + hex-decoded bytes (if present)
listValues[].valuelength(4B) + hex-decoded bytes (for each)
precommitmentValuelength(4B) + hex-decoded bytes (if present)
statusCode4B 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, outputValue are hex strings that must be decoded to bytes
  • uri, version, timeStamp are UTF-8 strings

4. Integer Sizes

  • chainIndex and pulseIndex are 8-byte integers
  • cipherSuite, period, and statusCode are 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