[{"data":1,"prerenderedAt":1154},["ShallowReactive",2],{"post-\u002Fblog\u002Fsip-trunking-stir-shaken":3},{"id":4,"title":5,"author":6,"body":7,"category":1137,"coverImage":1138,"date":1139,"description":1140,"extension":1141,"meta":1142,"navigation":205,"path":1143,"readingTime":248,"seo":1144,"stem":1145,"tags":1146,"__hash__":1153},"posts\u002Fblog\u002Fsip-trunking-stir-shaken.md","STIR\u002FSHAKEN Compliance for SIP Trunking Providers","Tumarm Engineering",{"type":8,"value":9,"toc":1127},"minimark",[10,14,18,21,26,37,40,44,47,103,106,110,113,129,132,269,273,276,454,457,494,498,731,735,738,744,857,860,864,867,1052,1055,1059,1120,1123],[11,12,5],"h1",{"id":13},"stirshaken-compliance-for-sip-trunking-providers",[15,16,17],"p",{},"STIR\u002FSHAKEN 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.",[15,19,20],{},"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.",[22,23,25],"h2",{"id":24},"stirshaken-architecture","STIR\u002FSHAKEN Architecture",[27,28,33],"pre",{"className":29,"code":31,"language":32},[30],"language-text","Originating SP          Intermediate              Terminating SP\n(your platform)         (optional)                (PSTN carrier)\n\n    SIP INVITE ──────────────────────────────────────► SIP INVITE\n    + Identity header                                   + Identity header\n    (signed PASSporT)                                   (verified by carrier)\n           │\n           │ signs using\n           │\n    STI-CA certificate\n    (from approved CA list)\n","text",[34,35,31],"code",{"__ignoreMap":36},"",[15,38,39],{},"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.",[22,41,43],{"id":42},"attestation-levels","Attestation Levels",[15,45,46],{},"The Identity header includes an attestation level that signals how well the originating provider verified the caller:",[48,49,50,66],"table",{},[51,52,53],"thead",{},[54,55,56,60,63],"tr",{},[57,58,59],"th",{},"Level",[57,61,62],{},"Code",[57,64,65],{},"Meaning",[67,68,69,81,92],"tbody",{},[54,70,71,75,78],{},[72,73,74],"td",{},"Full Attestation",[72,76,77],{},"A",[72,79,80],{},"You know the customer and the number is authorized to them",[54,82,83,86,89],{},[72,84,85],{},"Partial Attestation",[72,87,88],{},"B",[72,90,91],{},"You know the customer but cannot confirm they own the number",[54,93,94,97,100],{},[72,95,96],{},"Gateway Attestation",[72,98,99],{},"C",[72,101,102],{},"You authenticated the source but have no customer relationship",[15,104,105],{},"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.",[22,107,109],{"id":108},"certificate-procurement","Certificate Procurement",[15,111,112],{},"You need an STI-SP certificate from an FCC-authorized STI-CA. As of 2025, authorized CAs include:",[114,115,116,120,123,126],"ul",{},[117,118,119],"li",{},"Comodo\u002FSectigo STI-CA",[117,121,122],{},"TransNexus",[117,124,125],{},"ATIS STI-CA",[117,127,128],{},"Neustar",[15,130,131],{},"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.",[27,133,137],{"className":134,"code":135,"language":136,"meta":36,"style":36},"language-bash shiki shiki-themes github-light github-dark","# Generate EC P-256 key pair\nopenssl ecparam -genkey -name prime256v1 -noout -out sti-private.key\nopenssl ec -in sti-private.key -pubout -out sti-public.key\n\n# Create CSR for the STI-CA\nopenssl req -new -key sti-private.key -out sti-request.csr \\\n  -subj \"\u002FC=US\u002FO=Your Company\u002FCN=sti.yourcompany.com\"\n\n# Submit CSR to your chosen STI-CA\n# They issue the certificate and you publish it at a known HTTPS URL:\n# https:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem\n","bash",[34,138,139,148,178,200,207,213,237,246,251,257,263],{"__ignoreMap":36},[140,141,144],"span",{"class":142,"line":143},"line",1,[140,145,147],{"class":146},"sJ8bj","# Generate EC P-256 key pair\n",[140,149,151,155,159,163,166,169,172,175],{"class":142,"line":150},2,[140,152,154],{"class":153},"sScJk","openssl",[140,156,158],{"class":157},"sZZnC"," ecparam",[140,160,162],{"class":161},"sj4cs"," -genkey",[140,164,165],{"class":161}," -name",[140,167,168],{"class":157}," prime256v1",[140,170,171],{"class":161}," -noout",[140,173,174],{"class":161}," -out",[140,176,177],{"class":157}," sti-private.key\n",[140,179,181,183,186,189,192,195,197],{"class":142,"line":180},3,[140,182,154],{"class":153},[140,184,185],{"class":157}," ec",[140,187,188],{"class":161}," -in",[140,190,191],{"class":157}," sti-private.key",[140,193,194],{"class":161}," -pubout",[140,196,174],{"class":161},[140,198,199],{"class":157}," sti-public.key\n",[140,201,203],{"class":142,"line":202},4,[140,204,206],{"emptyLinePlaceholder":205},true,"\n",[140,208,210],{"class":142,"line":209},5,[140,211,212],{"class":146},"# Create CSR for the STI-CA\n",[140,214,216,218,221,224,227,229,231,234],{"class":142,"line":215},6,[140,217,154],{"class":153},[140,219,220],{"class":157}," req",[140,222,223],{"class":161}," -new",[140,225,226],{"class":161}," -key",[140,228,191],{"class":157},[140,230,174],{"class":161},[140,232,233],{"class":157}," sti-request.csr",[140,235,236],{"class":161}," \\\n",[140,238,240,243],{"class":142,"line":239},7,[140,241,242],{"class":161},"  -subj",[140,244,245],{"class":157}," \"\u002FC=US\u002FO=Your Company\u002FCN=sti.yourcompany.com\"\n",[140,247,249],{"class":142,"line":248},8,[140,250,206],{"emptyLinePlaceholder":205},[140,252,254],{"class":142,"line":253},9,[140,255,256],{"class":146},"# Submit CSR to your chosen STI-CA\n",[140,258,260],{"class":142,"line":259},10,[140,261,262],{"class":146},"# They issue the certificate and you publish it at a known HTTPS URL:\n",[140,264,266],{"class":142,"line":265},11,[140,267,268],{"class":146},"# https:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem\n",[22,270,272],{"id":271},"passport-token-structure","PASSporT Token Structure",[15,274,275],{},"A PASSporT (Personal Assertion Token) is a JWT signed with your STI key. The structure:",[27,277,281],{"className":278,"code":279,"language":280,"meta":36,"style":36},"language-json shiki shiki-themes github-light github-dark","\u002F\u002F Header\n{\n  \"alg\": \"ES256\",\n  \"typ\": \"passport\",\n  \"ppt\": \"shaken\",\n  \"x5u\": \"https:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem\"\n}\n\n\u002F\u002F Payload\n{\n  \"attest\": \"A\",\n  \"dest\": {\n    \"tn\": [\"12025551234\"]\n  },\n  \"iat\": 1701388800,\n  \"orig\": {\n    \"tn\": \"14085559876\"\n  },\n  \"origid\": \"550e8400-e29b-41d4-a716-446655440000\"\n}\n","json",[34,282,283,288,294,308,320,332,342,347,351,356,360,372,381,396,402,415,423,433,438,449],{"__ignoreMap":36},[140,284,285],{"class":142,"line":143},[140,286,287],{"class":146},"\u002F\u002F Header\n",[140,289,290],{"class":142,"line":150},[140,291,293],{"class":292},"sVt8B","{\n",[140,295,296,299,302,305],{"class":142,"line":180},[140,297,298],{"class":161},"  \"alg\"",[140,300,301],{"class":292},": ",[140,303,304],{"class":157},"\"ES256\"",[140,306,307],{"class":292},",\n",[140,309,310,313,315,318],{"class":142,"line":202},[140,311,312],{"class":161},"  \"typ\"",[140,314,301],{"class":292},[140,316,317],{"class":157},"\"passport\"",[140,319,307],{"class":292},[140,321,322,325,327,330],{"class":142,"line":209},[140,323,324],{"class":161},"  \"ppt\"",[140,326,301],{"class":292},[140,328,329],{"class":157},"\"shaken\"",[140,331,307],{"class":292},[140,333,334,337,339],{"class":142,"line":215},[140,335,336],{"class":161},"  \"x5u\"",[140,338,301],{"class":292},[140,340,341],{"class":157},"\"https:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem\"\n",[140,343,344],{"class":142,"line":239},[140,345,346],{"class":292},"}\n",[140,348,349],{"class":142,"line":248},[140,350,206],{"emptyLinePlaceholder":205},[140,352,353],{"class":142,"line":253},[140,354,355],{"class":146},"\u002F\u002F Payload\n",[140,357,358],{"class":142,"line":259},[140,359,293],{"class":292},[140,361,362,365,367,370],{"class":142,"line":265},[140,363,364],{"class":161},"  \"attest\"",[140,366,301],{"class":292},[140,368,369],{"class":157},"\"A\"",[140,371,307],{"class":292},[140,373,375,378],{"class":142,"line":374},12,[140,376,377],{"class":161},"  \"dest\"",[140,379,380],{"class":292},": {\n",[140,382,384,387,390,393],{"class":142,"line":383},13,[140,385,386],{"class":161},"    \"tn\"",[140,388,389],{"class":292},": [",[140,391,392],{"class":157},"\"12025551234\"",[140,394,395],{"class":292},"]\n",[140,397,399],{"class":142,"line":398},14,[140,400,401],{"class":292},"  },\n",[140,403,405,408,410,413],{"class":142,"line":404},15,[140,406,407],{"class":161},"  \"iat\"",[140,409,301],{"class":292},[140,411,412],{"class":161},"1701388800",[140,414,307],{"class":292},[140,416,418,421],{"class":142,"line":417},16,[140,419,420],{"class":161},"  \"orig\"",[140,422,380],{"class":292},[140,424,426,428,430],{"class":142,"line":425},17,[140,427,386],{"class":161},[140,429,301],{"class":292},[140,431,432],{"class":157},"\"14085559876\"\n",[140,434,436],{"class":142,"line":435},18,[140,437,401],{"class":292},[140,439,441,444,446],{"class":142,"line":440},19,[140,442,443],{"class":161},"  \"origid\"",[140,445,301],{"class":292},[140,447,448],{"class":157},"\"550e8400-e29b-41d4-a716-446655440000\"\n",[140,450,452],{"class":142,"line":451},20,[140,453,346],{"class":292},[15,455,456],{},"Key fields:",[114,458,459,465,474,482,488],{},[117,460,461,464],{},[34,462,463],{},"attest"," — attestation level (A, B, or C)",[117,466,467,470,471],{},[34,468,469],{},"dest.tn"," — destination telephone number in E.164 without the ",[34,472,473],{},"+",[117,475,476,479,480],{},[34,477,478],{},"orig.tn"," — originating telephone number in E.164 without the ",[34,481,473],{},[117,483,484,487],{},[34,485,486],{},"iat"," — Unix timestamp of call origination (must be within 60 seconds of current time)",[117,489,490,493],{},[34,491,492],{},"origid"," — unique UUID per call for replay detection",[22,495,497],{"id":496},"generating-the-identity-header","Generating the Identity Header",[27,499,503],{"className":500,"code":501,"language":502,"meta":36,"style":36},"language-python shiki shiki-themes github-light github-dark","import jwt\nimport time\nimport uuid\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.backends import default_backend\n\ndef generate_identity_header(orig_number, dest_number, attestation='A'):\n    # Load private key\n    with open('\u002Fetc\u002Fsti\u002Fprivate.key', 'rb') as f:\n        private_key = serialization.load_pem_private_key(\n            f.read(),\n            password=None,\n            backend=default_backend()\n        )\n    \n    header = {\n        'alg': 'ES256',\n        'typ': 'passport',\n        'ppt': 'shaken',\n        'x5u': 'https:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem'\n    }\n    \n    payload = {\n        'attest': attestation,\n        'dest': {'tn': [dest_number.lstrip('+')]},\n        'iat': int(time.time()),\n        'orig': {'tn': orig_number.lstrip('+')},\n        'origid': str(uuid.uuid4())\n    }\n    \n    token = jwt.encode(\n        payload,\n        private_key,\n        algorithm='ES256',\n        headers=header\n    )\n    \n    # SIP Identity header format\n    return f'{token};info=\u003Chttps:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem>;alg=ES256;ppt=\"shaken\"'\n\n# Use in SIP INVITE\nidentity_header = generate_identity_header('+14085559876', '+12025551234', 'A')\n","python",[34,504,505,510,515,520,525,530,534,539,544,549,554,559,564,569,574,579,584,589,594,599,604,610,615,621,627,633,639,645,651,656,661,667,673,679,685,691,697,702,708,714,719,725],{"__ignoreMap":36},[140,506,507],{"class":142,"line":143},[140,508,509],{},"import jwt\n",[140,511,512],{"class":142,"line":150},[140,513,514],{},"import time\n",[140,516,517],{"class":142,"line":180},[140,518,519],{},"import uuid\n",[140,521,522],{"class":142,"line":202},[140,523,524],{},"from cryptography.hazmat.primitives import serialization\n",[140,526,527],{"class":142,"line":209},[140,528,529],{},"from cryptography.hazmat.backends import default_backend\n",[140,531,532],{"class":142,"line":215},[140,533,206],{"emptyLinePlaceholder":205},[140,535,536],{"class":142,"line":239},[140,537,538],{},"def generate_identity_header(orig_number, dest_number, attestation='A'):\n",[140,540,541],{"class":142,"line":248},[140,542,543],{},"    # Load private key\n",[140,545,546],{"class":142,"line":253},[140,547,548],{},"    with open('\u002Fetc\u002Fsti\u002Fprivate.key', 'rb') as f:\n",[140,550,551],{"class":142,"line":259},[140,552,553],{},"        private_key = serialization.load_pem_private_key(\n",[140,555,556],{"class":142,"line":265},[140,557,558],{},"            f.read(),\n",[140,560,561],{"class":142,"line":374},[140,562,563],{},"            password=None,\n",[140,565,566],{"class":142,"line":383},[140,567,568],{},"            backend=default_backend()\n",[140,570,571],{"class":142,"line":398},[140,572,573],{},"        )\n",[140,575,576],{"class":142,"line":404},[140,577,578],{},"    \n",[140,580,581],{"class":142,"line":417},[140,582,583],{},"    header = {\n",[140,585,586],{"class":142,"line":425},[140,587,588],{},"        'alg': 'ES256',\n",[140,590,591],{"class":142,"line":435},[140,592,593],{},"        'typ': 'passport',\n",[140,595,596],{"class":142,"line":440},[140,597,598],{},"        'ppt': 'shaken',\n",[140,600,601],{"class":142,"line":451},[140,602,603],{},"        'x5u': 'https:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem'\n",[140,605,607],{"class":142,"line":606},21,[140,608,609],{},"    }\n",[140,611,613],{"class":142,"line":612},22,[140,614,578],{},[140,616,618],{"class":142,"line":617},23,[140,619,620],{},"    payload = {\n",[140,622,624],{"class":142,"line":623},24,[140,625,626],{},"        'attest': attestation,\n",[140,628,630],{"class":142,"line":629},25,[140,631,632],{},"        'dest': {'tn': [dest_number.lstrip('+')]},\n",[140,634,636],{"class":142,"line":635},26,[140,637,638],{},"        'iat': int(time.time()),\n",[140,640,642],{"class":142,"line":641},27,[140,643,644],{},"        'orig': {'tn': orig_number.lstrip('+')},\n",[140,646,648],{"class":142,"line":647},28,[140,649,650],{},"        'origid': str(uuid.uuid4())\n",[140,652,654],{"class":142,"line":653},29,[140,655,609],{},[140,657,659],{"class":142,"line":658},30,[140,660,578],{},[140,662,664],{"class":142,"line":663},31,[140,665,666],{},"    token = jwt.encode(\n",[140,668,670],{"class":142,"line":669},32,[140,671,672],{},"        payload,\n",[140,674,676],{"class":142,"line":675},33,[140,677,678],{},"        private_key,\n",[140,680,682],{"class":142,"line":681},34,[140,683,684],{},"        algorithm='ES256',\n",[140,686,688],{"class":142,"line":687},35,[140,689,690],{},"        headers=header\n",[140,692,694],{"class":142,"line":693},36,[140,695,696],{},"    )\n",[140,698,700],{"class":142,"line":699},37,[140,701,578],{},[140,703,705],{"class":142,"line":704},38,[140,706,707],{},"    # SIP Identity header format\n",[140,709,711],{"class":142,"line":710},39,[140,712,713],{},"    return f'{token};info=\u003Chttps:\u002F\u002Fcert.yourcompany.com\u002Fsti-cert.pem>;alg=ES256;ppt=\"shaken\"'\n",[140,715,717],{"class":142,"line":716},40,[140,718,206],{"emptyLinePlaceholder":205},[140,720,722],{"class":142,"line":721},41,[140,723,724],{},"# Use in SIP INVITE\n",[140,726,728],{"class":142,"line":727},42,[140,729,730],{},"identity_header = generate_identity_header('+14085559876', '+12025551234', 'A')\n",[22,732,734],{"id":733},"integration-with-kamailio","Integration with Kamailio",[15,736,737],{},"Add the Identity header to outbound INVITEs in Kamailio using a Lua script that calls the signing service:",[27,739,742],{"className":740,"code":741,"language":32},[30],"loadmodule \"app_lua.so\"\nmodparam(\"app_lua\", \"load\", \"\u002Fetc\u002Fkamailio\u002Fstir.lua\")\n\nrequest_route {\n    if (is_method(\"INVITE\") && !has_totag()) {\n        lua_run(\"sign_call\");\n    }\n    t_relay();\n}\n",[34,743,741],{"__ignoreMap":36},[27,745,749],{"className":746,"code":747,"language":748,"meta":36,"style":36},"language-lua shiki shiki-themes github-light github-dark","-- \u002Fetc\u002Fkamailio\u002Fstir.lua\nfunction sign_call()\n    local orig = KSR.pv.get(\"$fU\")\n    local dest = KSR.pv.get(\"$rU\")\n    \n    -- Call local signing microservice\n    local http = require(\"socket.http\")\n    local json = require(\"cjson\")\n    \n    local body = json.encode({orig = orig, dest = dest, attest = \"A\"})\n    local response, status = http.request(\n        \"http:\u002F\u002F127.0.0.1:8080\u002Fsign\",\n        body\n    )\n    \n    if status == 200 then\n        local result = json.decode(response)\n        KSR.hdr.append(\"Identity: \" .. result.identity_header .. \"\\r\\n\")\n    else\n        KSR.log(\"err\", \"STIR signing failed: \" .. tostring(status))\n    end\nend\n","lua",[34,750,751,756,761,766,771,775,780,785,790,794,799,804,809,814,818,822,827,832,837,842,847,852],{"__ignoreMap":36},[140,752,753],{"class":142,"line":143},[140,754,755],{},"-- \u002Fetc\u002Fkamailio\u002Fstir.lua\n",[140,757,758],{"class":142,"line":150},[140,759,760],{},"function sign_call()\n",[140,762,763],{"class":142,"line":180},[140,764,765],{},"    local orig = KSR.pv.get(\"$fU\")\n",[140,767,768],{"class":142,"line":202},[140,769,770],{},"    local dest = KSR.pv.get(\"$rU\")\n",[140,772,773],{"class":142,"line":209},[140,774,578],{},[140,776,777],{"class":142,"line":215},[140,778,779],{},"    -- Call local signing microservice\n",[140,781,782],{"class":142,"line":239},[140,783,784],{},"    local http = require(\"socket.http\")\n",[140,786,787],{"class":142,"line":248},[140,788,789],{},"    local json = require(\"cjson\")\n",[140,791,792],{"class":142,"line":253},[140,793,578],{},[140,795,796],{"class":142,"line":259},[140,797,798],{},"    local body = json.encode({orig = orig, dest = dest, attest = \"A\"})\n",[140,800,801],{"class":142,"line":265},[140,802,803],{},"    local response, status = http.request(\n",[140,805,806],{"class":142,"line":374},[140,807,808],{},"        \"http:\u002F\u002F127.0.0.1:8080\u002Fsign\",\n",[140,810,811],{"class":142,"line":383},[140,812,813],{},"        body\n",[140,815,816],{"class":142,"line":398},[140,817,696],{},[140,819,820],{"class":142,"line":404},[140,821,578],{},[140,823,824],{"class":142,"line":417},[140,825,826],{},"    if status == 200 then\n",[140,828,829],{"class":142,"line":425},[140,830,831],{},"        local result = json.decode(response)\n",[140,833,834],{"class":142,"line":435},[140,835,836],{},"        KSR.hdr.append(\"Identity: \" .. result.identity_header .. \"\\r\\n\")\n",[140,838,839],{"class":142,"line":440},[140,840,841],{},"    else\n",[140,843,844],{"class":142,"line":451},[140,845,846],{},"        KSR.log(\"err\", \"STIR signing failed: \" .. tostring(status))\n",[140,848,849],{"class":142,"line":606},[140,850,851],{},"    end\n",[140,853,854],{"class":142,"line":612},[140,855,856],{},"end\n",[15,858,859],{},"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.",[22,861,863],{"id":862},"verification-on-the-terminating-side","Verification on the Terminating Side",[15,865,866],{},"If you also receive calls from other carriers, verify incoming Identity headers:",[27,868,870],{"className":500,"code":869,"language":502,"meta":36,"style":36},"import jwt\nimport requests\nimport time\nfrom cryptography.x509 import load_pem_x509_certificate\n\ndef verify_identity_header(identity_header, orig_number, dest_number):\n    # Parse the header: token;info=\u003Ccert_url>;alg=ES256;ppt=\"shaken\"\n    parts = identity_header.split(';')\n    token = parts[0].strip()\n    cert_url = parts[1].replace('info=\u003C', '').replace('>', '').strip()\n    \n    # Fetch the signing certificate\n    cert_response = requests.get(cert_url, timeout=2)\n    cert = load_pem_x509_certificate(cert_response.content)\n    public_key = cert.public_key()\n    \n    try:\n        payload = jwt.decode(\n            token,\n            public_key,\n            algorithms=['ES256'],\n            options={'verify_exp': False}\n        )\n    except jwt.InvalidSignatureError:\n        return {'valid': False, 'reason': 'Invalid signature'}\n    \n    # Validate payload claims\n    if abs(time.time() - payload['iat']) > 60:\n        return {'valid': False, 'reason': 'Token expired'}\n    \n    if payload['orig']['tn'] != orig_number.lstrip('+'):\n        return {'valid': False, 'reason': 'Orig number mismatch'}\n    \n    return {\n        'valid': True,\n        'attestation': payload['attest'],\n        'origid': payload['origid']\n    }\n",[34,871,872,876,881,885,890,894,899,904,909,914,919,923,928,933,938,943,947,952,957,962,967,972,977,981,986,991,995,1000,1005,1010,1014,1019,1024,1028,1033,1038,1043,1048],{"__ignoreMap":36},[140,873,874],{"class":142,"line":143},[140,875,509],{},[140,877,878],{"class":142,"line":150},[140,879,880],{},"import requests\n",[140,882,883],{"class":142,"line":180},[140,884,514],{},[140,886,887],{"class":142,"line":202},[140,888,889],{},"from cryptography.x509 import load_pem_x509_certificate\n",[140,891,892],{"class":142,"line":209},[140,893,206],{"emptyLinePlaceholder":205},[140,895,896],{"class":142,"line":215},[140,897,898],{},"def verify_identity_header(identity_header, orig_number, dest_number):\n",[140,900,901],{"class":142,"line":239},[140,902,903],{},"    # Parse the header: token;info=\u003Ccert_url>;alg=ES256;ppt=\"shaken\"\n",[140,905,906],{"class":142,"line":248},[140,907,908],{},"    parts = identity_header.split(';')\n",[140,910,911],{"class":142,"line":253},[140,912,913],{},"    token = parts[0].strip()\n",[140,915,916],{"class":142,"line":259},[140,917,918],{},"    cert_url = parts[1].replace('info=\u003C', '').replace('>', '').strip()\n",[140,920,921],{"class":142,"line":265},[140,922,578],{},[140,924,925],{"class":142,"line":374},[140,926,927],{},"    # Fetch the signing certificate\n",[140,929,930],{"class":142,"line":383},[140,931,932],{},"    cert_response = requests.get(cert_url, timeout=2)\n",[140,934,935],{"class":142,"line":398},[140,936,937],{},"    cert = load_pem_x509_certificate(cert_response.content)\n",[140,939,940],{"class":142,"line":404},[140,941,942],{},"    public_key = cert.public_key()\n",[140,944,945],{"class":142,"line":417},[140,946,578],{},[140,948,949],{"class":142,"line":425},[140,950,951],{},"    try:\n",[140,953,954],{"class":142,"line":435},[140,955,956],{},"        payload = jwt.decode(\n",[140,958,959],{"class":142,"line":440},[140,960,961],{},"            token,\n",[140,963,964],{"class":142,"line":451},[140,965,966],{},"            public_key,\n",[140,968,969],{"class":142,"line":606},[140,970,971],{},"            algorithms=['ES256'],\n",[140,973,974],{"class":142,"line":612},[140,975,976],{},"            options={'verify_exp': False}\n",[140,978,979],{"class":142,"line":617},[140,980,573],{},[140,982,983],{"class":142,"line":623},[140,984,985],{},"    except jwt.InvalidSignatureError:\n",[140,987,988],{"class":142,"line":629},[140,989,990],{},"        return {'valid': False, 'reason': 'Invalid signature'}\n",[140,992,993],{"class":142,"line":635},[140,994,578],{},[140,996,997],{"class":142,"line":641},[140,998,999],{},"    # Validate payload claims\n",[140,1001,1002],{"class":142,"line":647},[140,1003,1004],{},"    if abs(time.time() - payload['iat']) > 60:\n",[140,1006,1007],{"class":142,"line":653},[140,1008,1009],{},"        return {'valid': False, 'reason': 'Token expired'}\n",[140,1011,1012],{"class":142,"line":658},[140,1013,578],{},[140,1015,1016],{"class":142,"line":663},[140,1017,1018],{},"    if payload['orig']['tn'] != orig_number.lstrip('+'):\n",[140,1020,1021],{"class":142,"line":669},[140,1022,1023],{},"        return {'valid': False, 'reason': 'Orig number mismatch'}\n",[140,1025,1026],{"class":142,"line":675},[140,1027,578],{},[140,1029,1030],{"class":142,"line":681},[140,1031,1032],{},"    return {\n",[140,1034,1035],{"class":142,"line":687},[140,1036,1037],{},"        'valid': True,\n",[140,1039,1040],{"class":142,"line":693},[140,1041,1042],{},"        'attestation': payload['attest'],\n",[140,1044,1045],{"class":142,"line":699},[140,1046,1047],{},"        'origid': payload['origid']\n",[140,1049,1050],{"class":142,"line":704},[140,1051,609],{},[15,1053,1054],{},"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.",[22,1056,1058],{"id":1057},"fcc-compliance-timeline","FCC Compliance Timeline",[48,1060,1061,1074],{},[51,1062,1063],{},[54,1064,1065,1068,1071],{},[57,1066,1067],{},"Requirement",[57,1069,1070],{},"Deadline",[57,1072,1073],{},"Who",[67,1075,1076,1087,1098,1109],{},[54,1077,1078,1081,1084],{},[72,1079,1080],{},"Implement STIR\u002FSHAKEN",[72,1082,1083],{},"June 2021",[72,1085,1086],{},"Major voice providers",[54,1088,1089,1092,1095],{},[72,1090,1091],{},"STIR\u002FSHAKEN or robocall mitigation",[72,1093,1094],{},"June 2022",[72,1096,1097],{},"Small voice providers",[54,1099,1100,1103,1106],{},[72,1101,1102],{},"Certificate must be from authorized STI-CA",[72,1104,1105],{},"Ongoing",[72,1107,1108],{},"All signing providers",[54,1110,1111,1114,1117],{},[72,1112,1113],{},"Annual certification filing",[72,1115,1116],{},"Annually",[72,1118,1119],{},"All providers",[15,1121,1122],{},"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.",[1124,1125,1126],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":36,"searchDepth":150,"depth":150,"links":1128},[1129,1130,1131,1132,1133,1134,1135,1136],{"id":24,"depth":150,"text":25},{"id":42,"depth":150,"text":43},{"id":108,"depth":150,"text":109},{"id":271,"depth":150,"text":272},{"id":496,"depth":150,"text":497},{"id":733,"depth":150,"text":734},{"id":862,"depth":150,"text":863},{"id":1057,"depth":150,"text":1058},"SIP","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1487058792275-0ad4aaf24ca7?w=1200&q=80","2025-12-01","Implement STIR\u002FSHAKEN caller ID authentication for SIP trunking: PASSporT token generation, attestation levels, STI-SP certificate management, and SHAKEN verification flow.","md",{},"\u002Fblog\u002Fsip-trunking-stir-shaken",{"title":5,"description":1140},"blog\u002Fsip-trunking-stir-shaken",[1147,1148,1149,1150,1151,1152],"stir-shaken","sip-trunking","caller-id","robocall","compliance","jwt","EsDNmAwSo7VBbFj6VNn5alnkHRUGBYzzxrfM-PXsTvs",1776974166862]