STIR/SHAKEN Compliance for SIP Trunking Providers
STIR/SHAKEN is the FCC-mandated caller ID authentication framework that SIP trunking providers must implement. STIR (Secure Telephone Identity Revisited) defines the cryptographic mechanism. SHAKEN (Signature-based Handling of Asserted information using toKENs) is the industry profile that specifies how US voice providers apply it. Since June 2021, major carriers require originating providers to sign calls or risk having traffic flagged as unverified — meaning customer calls display "Spam Likely" or get blocked by call-screening apps.
This post covers what SIP trunking providers need to implement: STI-SP certificate procurement, PASSporT generation, Identity header construction, and the verification flow on the terminating side.
STIR/SHAKEN Architecture
Originating SP Intermediate Terminating SP
(your platform) (optional) (PSTN carrier)
SIP INVITE ──────────────────────────────────────► SIP INVITE
+ Identity header + Identity header
(signed PASSporT) (verified by carrier)
│
│ signs using
│
STI-CA certificate
(from approved CA list)
The originating provider signs the call with an EC (Elliptic Curve) private key. The corresponding certificate, issued by an FCC-authorized STI-CA, is published at a public HTTPS URL. The terminating provider fetches the certificate and verifies the signature.
Attestation Levels
The Identity header includes an attestation level that signals how well the originating provider verified the caller:
| Level | Code | Meaning |
|---|---|---|
| Full Attestation | A | You know the customer and the number is authorized to them |
| Partial Attestation | B | You know the customer but cannot confirm they own the number |
| Gateway Attestation | C | You authenticated the source but have no customer relationship |
Use attestation A whenever possible. Carriers downgrade or reject calls with level C from unknown providers. If you're a transit carrier passing calls from unknown sources, C is appropriate, but calls will display as unverified to end users.
Certificate Procurement
You need an STI-SP certificate from an FCC-authorized STI-CA. As of 2025, authorized CAs include:
- Comodo/Sectigo STI-CA
- TransNexus
- ATIS STI-CA
- Neustar
The certificate is a standard X.509 cert with EC P-256 key and an extension that identifies it as an STI certificate. The CERT URL (where you publish it) must be reachable via HTTPS from any carrier's verification systems.
# Generate EC P-256 key pair
openssl ecparam -genkey -name prime256v1 -noout -out sti-private.key
openssl ec -in sti-private.key -pubout -out sti-public.key
# Create CSR for the STI-CA
openssl req -new -key sti-private.key -out sti-request.csr \
-subj "/C=US/O=Your Company/CN=sti.yourcompany.com"
# Submit CSR to your chosen STI-CA
# They issue the certificate and you publish it at a known HTTPS URL:
# https://cert.yourcompany.com/sti-cert.pem
PASSporT Token Structure
A PASSporT (Personal Assertion Token) is a JWT signed with your STI key. The structure:
// Header
{
"alg": "ES256",
"typ": "passport",
"ppt": "shaken",
"x5u": "https://cert.yourcompany.com/sti-cert.pem"
}
// Payload
{
"attest": "A",
"dest": {
"tn": ["12025551234"]
},
"iat": 1701388800,
"orig": {
"tn": "14085559876"
},
"origid": "550e8400-e29b-41d4-a716-446655440000"
}
Key fields:
attest— attestation level (A, B, or C)dest.tn— destination telephone number in E.164 without the+orig.tn— originating telephone number in E.164 without the+iat— Unix timestamp of call origination (must be within 60 seconds of current time)origid— unique UUID per call for replay detection
Generating the Identity Header
import jwt
import time
import uuid
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
def generate_identity_header(orig_number, dest_number, attestation='A'):
# Load private key
with open('/etc/sti/private.key', 'rb') as f:
private_key = serialization.load_pem_private_key(
f.read(),
password=None,
backend=default_backend()
)
header = {
'alg': 'ES256',
'typ': 'passport',
'ppt': 'shaken',
'x5u': 'https://cert.yourcompany.com/sti-cert.pem'
}
payload = {
'attest': attestation,
'dest': {'tn': [dest_number.lstrip('+')]},
'iat': int(time.time()),
'orig': {'tn': orig_number.lstrip('+')},
'origid': str(uuid.uuid4())
}
token = jwt.encode(
payload,
private_key,
algorithm='ES256',
headers=header
)
# SIP Identity header format
return f'{token};info=<https://cert.yourcompany.com/sti-cert.pem>;alg=ES256;ppt="shaken"'
# Use in SIP INVITE
identity_header = generate_identity_header('+14085559876', '+12025551234', 'A')
Integration with Kamailio
Add the Identity header to outbound INVITEs in Kamailio using a Lua script that calls the signing service:
loadmodule "app_lua.so"
modparam("app_lua", "load", "/etc/kamailio/stir.lua")
request_route {
if (is_method("INVITE") && !has_totag()) {
lua_run("sign_call");
}
t_relay();
}
-- /etc/kamailio/stir.lua
function sign_call()
local orig = KSR.pv.get("$fU")
local dest = KSR.pv.get("$rU")
-- Call local signing microservice
local http = require("socket.http")
local json = require("cjson")
local body = json.encode({orig = orig, dest = dest, attest = "A"})
local response, status = http.request(
"http://127.0.0.1:8080/sign",
body
)
if status == 200 then
local result = json.decode(response)
KSR.hdr.append("Identity: " .. result.identity_header .. "\r\n")
else
KSR.log("err", "STIR signing failed: " .. tostring(status))
end
end
Run the signing microservice as a local sidecar process. Keep it on localhost to avoid network latency on the signing path — adding 100ms to call setup time for every INVITE is unacceptable at scale.
Verification on the Terminating Side
If you also receive calls from other carriers, verify incoming Identity headers:
import jwt
import requests
import time
from cryptography.x509 import load_pem_x509_certificate
def verify_identity_header(identity_header, orig_number, dest_number):
# Parse the header: token;info=<cert_url>;alg=ES256;ppt="shaken"
parts = identity_header.split(';')
token = parts[0].strip()
cert_url = parts[1].replace('info=<', '').replace('>', '').strip()
# Fetch the signing certificate
cert_response = requests.get(cert_url, timeout=2)
cert = load_pem_x509_certificate(cert_response.content)
public_key = cert.public_key()
try:
payload = jwt.decode(
token,
public_key,
algorithms=['ES256'],
options={'verify_exp': False}
)
except jwt.InvalidSignatureError:
return {'valid': False, 'reason': 'Invalid signature'}
# Validate payload claims
if abs(time.time() - payload['iat']) > 60:
return {'valid': False, 'reason': 'Token expired'}
if payload['orig']['tn'] != orig_number.lstrip('+'):
return {'valid': False, 'reason': 'Orig number mismatch'}
return {
'valid': True,
'attestation': payload['attest'],
'origid': payload['origid']
}
Cache certificate fetches with a 1-hour TTL. The STI-CA certificate changes rarely, and fetching it on every call adds latency and creates a dependency on an external HTTPS endpoint in the call path.
FCC Compliance Timeline
| Requirement | Deadline | Who |
|---|---|---|
| Implement STIR/SHAKEN | June 2021 | Major voice providers |
| STIR/SHAKEN or robocall mitigation | June 2022 | Small voice providers |
| Certificate must be from authorized STI-CA | Ongoing | All signing providers |
| Annual certification filing | Annually | All providers |
File your annual certification at the FCC's Robocall Mitigation Database. Failure to file results in downstream carriers refusing to accept your traffic — a business-ending consequence for a SIP trunking provider.




