From d7980c74afdab8a395eb13fc9d119f578252b20a Mon Sep 17 00:00:00 2001 From: noxlesh Date: Tue, 1 Sep 2026 17:29:28 +0300 Subject: [PATCH 01/16] docs(proto): state the Ed25519 delivery-URL scheme in the file header and DomainVerificationConfirmation The file header said signed delivery URLs use HMAC-SHA256 over a secret shared between the Exchange and the CDN. No implementation ever did that, and the SDK in this repository has never contained an HMAC: signing is a detached Ed25519 signature over "GET\n", base64url with no padding, and the delivery endpoint verifies it with the Exchange's published public key. Nothing secret ever reaches the edge, which is the whole point of the scheme and the opposite of what the comment promised. The identity-binding block is unchanged in substance -- the agent thumbprint is embedded in the URL and covered by the signature -- but it now says which signature covers it, and the bearer-only fallback no longer names a primitive that does not exist. DomainVerificationConfirmation carried the same claim twice. signing_key said key format is "PEM for CloudFront, hex for HMAC"; hex is an HMAC-secret encoding, so the sentence had no reading that matched the implementation. Its format now follows cdn_type: a PEM RSA public key, or the base64url raw Ed25519 public key. cdn_type's documented value set becomes "edge-ed25519" | "cloudfront". The retired values named vendors rather than schemes, which is why "fastly" was actively wrong -- a Fastly Compute deployment runs the Ed25519 verifier, so its scheme is edge-ed25519. The name edge-ed25519 is not new: ADR-012 D3 assigned it for exactly this path, so adopting it collapses two spellings rather than adding a third. The value list also moves into the field's LEADING comment: the website renders leadingComments || trailingComments, and this field already had a leading comment, so the trailing list was invisible to every reader of the reference page. No wire change. The value set lives only in a comment on an optional string, never in an enum, and cdn_type has no consumer in the SDK, the conformance corpus, or the reference implementation. gen/ is regenerated with the pinned buf; the conformance corpus is unchanged, as comments carry no constraints. --- gen/descriptor.binpb | Bin 597493 -> 598055 bytes gen/go/ramp/v1/ramp.pb.go | 11 ++++++++--- gen/python/wire/models.py | 7 +++++-- gen/ts/wire/schemas.ts | 2 +- proto/ramp/v1/ramp.proto | 31 ++++++++++++++++++++----------- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/gen/descriptor.binpb b/gen/descriptor.binpb index 0701418b972b6a944f879864e551cde27b3c44ff..ff34766f9a2103f049c25993d5f84d664d661e5e 100644 GIT binary patch delta 42002 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 Y^~-|io}V{=-n7oXA(LWv^bR%gf8M?L`Tzg` delta 41543 zcmZ5pd3+Q__RsY6bocZmnVFs;b0nEexQ!&-cY+{z-{FNHUa(#U2?FT&{3^dpF&oFh%6HD7P{pyb%Ex6=yT8);tu8MnyLWlvtu8$pBvYRs_SAJHx_0Z{^~Em99KW+(f-`)RWyL`Nm5xz1Fo`mo~{4-stvH*SB8h%242?C+olY)*J71 z>CTz2bngBN>0LVNsm+qdJoOWxl5f8K{A*obNWR+Ty=3QZFXCVF(K|aPTQ_Ucn61dx zve^5_JR4MM348jur)DVG_4PMj>+*V+Zaq5p==w&t?tn-QIqvx?$N#O%h?p;!s+Ojt zS?Wgin67mx-00FGW`^k@rUI@jTE`~cL^f(~cIGNBOWneL(X|%}x45*ZlyO02iz{}k zO+sahtHnK11rN8gw+-!uqU|m%C}kAlx~yEAgvxeT{kCSMn->FePfGJ@V_ijioO0Yy z+T)7VlS*!qay98-u6J=I)lv_zO{TW7=zvShlR_$V4!Dx7Z4x>MT%DdZ2MZmKr5qpEPs;Q{(+ulmrP{kGBr6fDB z=LUJgsrCUaL-F@kj9l|2Q&o8v_GSNLX|FWyt0>&K#6&OkRV-c#k$~;1#Ny^8R0Xul z(grBX5ca#JO)SMPlZq!kj4?p**(ffc3{WiI$@CN`1C;C>(=UV#LdhMXxXqyHrRPR} zuZ8xMDQY7#WcqBDnhEAWMQ!unOrQ-?)V4MMn+wwpR@4X0vo06_^{i0R4zQMC?M$#j zF>07q)BqRxZKc9ilo?}Nfk3S*6~*9<0Et>xD&BOPL^+j;KVZJcHSzFcMTy5>W$icV z_pm3kw8BC#d{Ua4{IQZ=!zQ8pu@a4$4Y)GeQN`wFX+_Ug*+z3I3W~3?jRq13RkrCt z0-=gbPZkDg5Se{1HyM$_WK~MeJ?3nM7XpQ8sgj7+&{ZcW2xe+C6!=akhL4wBrujL4I7y=;pC_)nzb=? zB%#e#I&E|?Jj8Zd>jB0_#i}haXrOFV>eV;LineH$woOrXDr&rp74Skt!eG{J2o+3ll_iO3;^v{EFK3?83RE3mHb-fXaz2D0N} zTErDRrWjG-(+c_Y3E|T%K>+fEZ6=U_JYoAZNI;$-pWaS&BbU=oE6RCxs;>5ZDSip- zf)BORwsk>5?X(iE!L17t);+CcotM@n2Dk1RMg3h`SMs2D*0!!>!Mf+TbxU{+FfuMF zN>03Cl69!BweAE$1QkVrBA)pI+^!%|r%OtZPd2DJjgL!8Jlh;3nga5wqST9jkbH=p z-k>Mg?)qBS60mGI8VFYvpBSM~2i#R9tENP!8R4o@r>=RP98gg(5c;Ni-=e+iD(UOK zfk6YIuiGbj9zwwA>yC+W0TLK}-F546$Kk{KBR3m9S&ugd0nLxx(q}+I^CP$P8IaKY z$Zh)!Z}||n@R`yE@R=b_6*P445Vs7qkOa^WxAYm102<+-Y;!$_84> zKgw-b<`Za~n>?n14Jg$9Qdr@ZeGVp&D%>*sfdo>88?N#QSHZ(dmQkcVR#Iu##jUu| zs7kl&A&?~GmOTU#2$gR1P=A#}psOdPJ}%O{O2yhqM>Fj&iTQ4Yk7#1T1oPebHORWm*ydJRjnt^t+FxA>u()%O02DTpb3DW4(Za1Py{-0K zVyj!>y#*O+;a2J`xG_i!p6^*%TkWyD@15FF()K;oZVysO2%-CTrLMHq_PX+Rxs_XF z^(af*?X()M>WY{E(bjlqF znytAC9-d||cF@`uo_17tFn1ebs#HZVcO#g0;VP)gIkusL_HyAl>4aipP|vw@#O#b{ zh*HkEF*`rZRX|-}&)u#)+U|lqXjDG%F1Tfm01{QX;I3UD>q!Ieg1fNDT&bbO9wdNj z+Q+HiZr5&i`v<7VNt*)MLrosYI^3x>FC3`Kzyv0s2C7~WbU^}Ypo(m=8CSu>A#C!U zT8qLVsrro7AD;(~qkmC+l5hBUX9^T~PYK<(j z5my1gDE9JQT9d+2jtaGLl!`TxP@y)CQW5V&#o&&Nt+`8UQNmOio;(VL7N+`yVL<|c zsdYqX0SN@A!dX9~In6`fI*zrzTbtW%oC86L)Hqd60FVL1I8|osAOSH>McO#t4MQW1 zny#i@O4YkZ``8tjuEK9dP~_9d(z8ZdPg|lo^gA1@(s?4fD0&JEFSGj}h;Ncur z<9_X~k~wyDG@km;v8w};gj7q|1SAmVkWJQLXrWk`K9#DxU#oT%rc}ktEe9@?QYz9i zG9*YSrKs43TnE%bHsnFAap6K$5!ryo{cE8b6lpC;5>ji`Hal!NwqK&&+JJkaMy6lRUVTVwUb5UlphaVC!~RAND?V*xrY=0J{aY=?6OpfJJO$n1^c$t1JIH{v;KEbnYdh4e-KxljiNW)= zovOM^71^-lLGMSHiD$!-g?#NNm9pX830@^yhj^4od}HeD6WY`MA`o^hD1WW;c*PLX zc>Y@D!4TRi?BTW4sqCL!wC{_iIyzqHO!at0@~kOd)L^Oyi4b20Xo_j6GgI%sq~*Fg z&h*Gp1TK1Krbi9|kieMfvG~M?=9<8m>8V}UoG!}sS?XMmQa3)@#eN=#{f;v)YlBO` z5h;VF_<%OoLGuA^uE(-H%LlZ%o?3OxMFwwwx}~m4Eq+BSQA<`i80qNNRh}EZl@5$m zo;;DoXi7RTR(XmVnad~>(v%GBa->?ku60mKwm29Wz}VuEu>nHB*y6e2T^YdG;%U;% zJR@3_i7sYezM-8f+2KHB0%C_pF7`Af6A(K*`AwyRW&&b|r$sCCn#QZ)x76dQU*6Pu zy6-sdVEBP?+~c!*(GQH{o{;UZeqbE;B$DQLq8b59{ndlj(K1)^eztV19%bM3(B_ta zXPY+wyk9*&v8A9X0pR`WiP#+;0N$_E;k4M&l%S=a@raxm1T@chWX=o{nrA#RX9fw) zGo*P<)WovXiyodcccQ79s2g>#(8U*>F1CPl(Ib;)2m|S&2T5~<7cmoKZlsp>N2=}r zY2|8Qq=tpoC@Mjxm^qs5`~WTX7N2FQUUa6t!6FrZjOM5 zW7!w)YxkCnbr5LtWUPZg5p=9(iFJz3Bj{KSI|(0(N}y>ISnGdl{fZ|z5EQN^I1m(1 zCuo-FGo8oN2^vz$yLcr~rAh3Me{1)YOmYz1K$zqp(9||ble3D>!|Nmsv&v*z;_I|J z`Xbfq18u5O{Dp&od0qX&!Jx_Q3r$)XIwuwQA(eDfonL%=<%Q^DR^nfrxmO$o~mp^#dw)ev!A=OMLPLtLl11tj8E zwO06)8Nj{(k^Q}J;cBhuF9>?3&A|>Dc#l6}4ixQ0uD3IFt*A2IWt&ZC6bZSfc z06U$ory#tOx_%K&eL8L69MBMkX1UqJx9N%0iI22OR|#0QCmCqZ0ZlGYArH6%T9$}v zAOUwkLpr}+z#*ERNPSbLZFH5Ka4@{6(g~+Z6kSg^T|m+Gga${SDSQdh^)%ZsNSj@9 z+JT_pdfI`Y;CfoKcqWflP;fo1)i2=VRaX#O&uM(Tvdx1vt>HOM4p%&a;yFzYSCCLV zr`f|*R}e=pYhsCeZ3vdQmz^4@1Ap}-%N~FOl@QJv~{W5hia;x zHC8vW&4rZV7&KZZ>Pl98rZTdsB$j%+0+UD)IAXTKbHGm2Wv&AfuoHF6nG?yr1`VMZ zspU+Y>?)e!DAC}Yp?fpAH$ezWGxVTkeoO{6D7~4@hF5A6ie~F#MMq5N%+_=G-ZPQV znXMPxDmE?*B0p!(jM0V^eeRS)dJWjlboo_*jj(iu|to(IQy2I<ev^sUgHWO$b)afbgaE(gd0<(Zd5!ZG0#!PK%-gRjg z;zH^=Nex2%K!fa=dTy3B#Fgx6D7+QKCAI;n4M=Da0knZe+VWK9XWDvKje!Q%+e@ho zzNazBP-@kj!yZo8bC@|-n@|%BvGm4czzi}FeMSm#taXPO$_%!Ct~R0+zr^wzn--88 zW*}A~bpk3n7;?!OhMNKgXMEoeb{%f0O=%mEbGG(#!yY3HHF+}&jUH*JwQgnsdz7Kp zxtRs5^3p%?(7P3XVxjNYpTH0;MjL9)o52vKDh;*q%`8NnF@LHUBF9)mt>;LU_CsQ* zjx*Hy4zHgZL;GVxt$Q;IV|M(XDuz^M!k?s&nNP2vFn76nr-AIr4wL?LNfo08&06Oxf6V5sLfwt${*C`(X@e}1VoDaCKQ612N9->C#b03`F5+^B?GRN^Z`Et2qVS&5Wmea=E9 zzIM8evw*$ePg3y2h0P50)|xk2(w@sQ z11t1LFk-R0vC>Fe$5zhQ9=28*o&b*^e0O6NMmdilwZ*;$Xub{T0SSmzMlQ}TQ4IDh zXerh0E3IZ?wW09Lh?tOBO)^LwL4sPtKKn|`PpmOSGC)k?l1x6AK^xbylV54I5^D{G zXQYrJL$4*7LN0?QRkNa$)-bQy!J#HqlT0Tx3ELMNjI{6B`zfuXe}mx(@h0)zi;e8N zlvb43XefLK(gP+mHyYR;yC3M-im=r0So~}4!Nhlt47KDtlEMB8WI%0V{l3;7O>D9Q zmfXzdeQ47pv0JqDm}A(<9j2Hm-p zon4@{N^G?|5Ss{)*-A3Ea~VXMZS0g+QNKW2Kj>cyhqkf9tkRP(sY)Is<6BIa`* zi!9NOHT%U-3wTZ`9s%;Op|-w>gX$e2&PB>7y)=PdNHtrkT~^v%Fk}h{F1q7_Ar})M zVVetvTnc~$#svdQfg=cDz`)Zh277IVcC_S*gF(xQD-H%_ELRLmWXI3}#uXzHZDV^!<4e;%2B1y-K?dxTf&Ulf|Xd}H!dFr#3+EVxP_$g9iJOIo{?+trUf-}-< ziM1o10%oLFglR%SE;!PgU+xv>%EZ7n2b_15SAE@F;YaV9mbSo~_H*jHwc2vM_5v^8 zA#|&_`JpZHD$Beo+N6UNy-O_i#U|~Eri;Cp?rFs!9>Ho$yy_h{aZr<`UbQ{B84lpl zx!tLet-*}}JK|A%rmGY`h2ii3hO4#>2NGphd*y-r}HBdj;3w)D%)Av zR_)=J@yo6#x!Pu@qU36u?H+~}pf}q+3=$RH?9JbPqoVdP_!g(4oW(1;m37#rh3jlX z)nv`&Gt}&Rsu^FHnN-G})IYarOI!{2comJiEV!u29$OW>rEf~#p&f9S;-^?{-~sS1dA$M;B=9Z~9&OQqM9VID z!+lMjXL80i$}=x})dn;nbIul?Xa3<88Klhu?iH`dGi?_9^Qsq#Na;p96*6L-7tj1;`&+Kigxi=Is%mClVys55ye>P9;x28>Ljz2?Qx7I_O?jRH5;y})oM%wn0EtmCz_eP(lE^P9Bn&V^%}qK#_E7*E z%=YirZYv#ZD&kZGOn?qHGeyh;3DChNVx9n{mH1#Y)`<@_JQYK2uo>^jhnnQjP#bKv z?_l<)4o|lrjwA7HoZp$;yROh(NdT41n1(hoH z`Cjdo=c`P)4Gtz+T4e^sW)VoVw93Q`K`jM|mR6Z@5vYjdL8Z#f?!;RvIn>fBv%~FX z5ibXblUeaTt>=J4==^nS9Lf3GZu zI*tdg$>v>mo1fBpG@ZI`W@^(uZI-KerYQpgxbU}`rrdr6iR#QWLt-};BnqBsVxJ#b z5FkL!VW|UJuXb}B1e$y1m~zs93=rm+@-h-g(lL>oF7=^bGmy%eY5h{~9MqP0>TEFO zgph&G+hi)+(0LPRzR!@oM?Ch_F>U<4n@zO|Z?1TRwrnxg2YBxZ4tj5^sea^wiQLe) zv>#06C*TdcfrsEtJl6iWHnez$sWx=*DE--KBF@sB!|}-CKbmS6a}y1w4DyD(rji$5 z?n>Up7BALftoDDjxy9g#$O;J}~DiG-F8t1`&S2Uy^Q*5ZW& zrXpe#n8Y>ZtPK(>2TaV`)afA6=?BcjJ)(|wrynr$?&h6N0{ONd{IDsvh%a1iKp=!7YE-!k}K$_Z2MzJUb72@|_-!^n3t zXx2KHdi#_%!u8BK2ZPRs&N(Kht(9|*$!YuY9GP6$&jb6NGxNmY1R|^zBTllDxrK7X z46;h^RF~gylj)h>z8n2Y7eIRZqp88K+s}MRnLj}FGs#8Au)kl>dKQiG$!QQwK#lRqjV+LX8so#p7M~+B$qy$m-$iXq z;RHvA{BVNLD`F93pfkaT(_BinGAWEqVp}h2?TRPa4FD4jnBZ+El}~-<2HqN<@-6U!HXK5kWZgiaPFw3kD&cT|TkS(K=5TnG?yUxQ#wJnM)2$ zaT|S@%zKfs{p1c?eaZ*%Gp^*rZ*_g4OR{S>eu0|qQsa8|TjjUuN1lg-NZugN6}%$9 z0Es$m^~o#+BDj}z6#oU$u*)}<$0rMqk&@hjx1yKEl;35;Dnnd^fD#x5VG>P$p!kOnY( zN!(qctdD3>*G>jsIe`;;&Hl22sa z#Nb)?uRfW{5=Ug+zxl*29@hd3xWD;ow&PYTm15}9-+b-=DlN-3QQm2v%#@`REP2{j zv+d1NsPk!G`@h}HLhL{Dry>!l&-iMV+$;shJL79#`d^yZ-aYG+`MN9;ySwN9R0}$| z@zuQhW-#pT{_YbS%62QUyZgJZ_J0+L-QC}P?Vr9`u?@C&&)(xD2{*AS=qypHB=-mm7vSZiUQ1 zl6i{DfEvU;%h3Ot7?dveH*uayT#~^O1ZAO0gIV27{kG&_2ZuJE29wNLswn)dG94G5 z`ey2HDgMfI99z(xHGW7nhG{|lxq4&L)&;`&<*!(>_ginLg9) zp>x6s>Db8ZiURrOR8_iCv*zDe@LeO$>WB3E6JUrfO*{rrRXWaL>GmPthML4ig!KFJ zCfPOu6Ec(1k%#o58u5*$sq9Kf@0&L@T^`DS38AU!IFvakgkZ@TtSYSklAK}p8a9g{ zG=nU8Gy{`dz|uCRr&Xk;*3ge={*CFl*7g7bLV)IuO>AtA{!rsh=`wDEiFR#D_lisU zAW^`k^h`g$=m#B4TRYR0J*n(meYdL=KSjL91L*Bcmy30f(A$|V7waI=ojcRRd(y>y z8e+h`E$zp2b$7bBPa}EI`w3S=cwm(*+^5-LFf!dIWvsHEOcd$v!;4GaqZeKKxfQ26hY zA(PSo5C45KWKs$eRqvA_lTso@Qrag2N$GUTuLCqg3`)IMPoLo`8RV2g`SqX-nWIAp zr3}i*5^F<{sOg{#tPSZZNr1BFp&8;T2?$QflTwh-9GW4|ra?k;Xoh_@9iW`JJcFN2 zcLaeJmpgT!)VSPfDW%5c8S;_?gn?9^fx~IMKIXx0V`WC#RCc^Tzb{akArDWnm51S- z!REeh)T<2w0An)bsSijv#Fz}^)}1hHu$M*ydDyM`or!T7^1uR2$c)QCmk1e9I^y5z(f}~@Ih)uSGf<&pC$P$lo1W;Sq&n@)Fi?&LO z0+znPaOePIcLriHzc3dhuRoA_wKXQy;sXu_ zx&MI-c{vwCz&MamyAfX`f&|8ajHb2}~*_HD5~qvWguLHAeAIuLY!<*d`wbbsY+Ms_X_5<$AZ za$W`r5U|pDIXuuEbb;l(9V9?P^Zbn<5u^(&S45EL1OnZE#i;|`V7cPdfo`x|v4aGJ zfpmp}#MhJ?U@v(-?(2vRWYBz|ctFC<%Q7)g z=(Z_n6fu}x?4Zxg8=NVRNWp~A;7r(jmk%0Z%7F^Z5V=kbxZSFo;~kn>bz%G?P|$W&xvhWZ`LQ4h9ECuOG1NmX~! z&$$AVGVwaWVJZN-j4eEZhdRsrZT`abtdj~PlmE;@tBpVbcl~~v1?CgZP?a_^ezuoXUgz}=cs3O zro27_5`|S~;`$72zktNRsLqVE=h-rm$P-X-b!PN7eq-OFZbN{3SZ~{Ky#vYvT&BDf z3mJf}r&iCP`-ve-+n1SkD)rmL`X_2&U#5}ZcUsgC)%76zvML~<*Vm7#(1H;pGQYn5-2Q&CU?c`*eri9zTFm=ca$uQ$ z{#u1MvjEi3->@yWdzf6b%ujYtKHB9UtP|Nj|L=N}A}~d=3K7(#%rD<41qpyMKWvY^ z9%=!|5vg8(*C)B28R3_C7`O;wBm8pp1QHk{{MjNy0!d1K#82{0knqkC{yJilf=J|H zT%qnGJUoVJn3uC&&+7R_<$jqwfeD#%zf7J$0<_$3Cr@FT#l}wG{H*RO8SB)GCb6-8 znIA$15M%u^YXS*~v3>-^r_tXvurl$d&0-my^``<8{1}RZX?mi3ud2rugO73`jI_ir;F*_ne62o2*m(p_b+efeQ7Rscp~e-CXTv+V!o$ z-)x&{+Yuy5`Q=+_AW_InKVCeVN)DQZi#h(Zt*O!%^(n64d_S%ratF<#8h`EQ4$3yY zq-#ZA+kS>eK>XS-{Szb*zxKmFDMYGS)b~r6`(^!T;SxuOYO%yG!vbWWvxE%)I9Gv7 zEoaeJ^e2m#+wI8Wrzy+*@PWiMaw8eHl z2m)t|?R+4CvxS`RZ|KX2rET}8^4Av_2do~e@$=FWQW}!U;=1| zU+&g{1kesYcI)U}tO%LHxID}-Sj$zhp>~x2V8^?|3iLVgej0vIpoKh zn$BY*)O|%(&#_p6iOX=#4q31fC>0|X_wXZ_7uNS8r=d!7yb zhu);!c?ZD@g!6v+jz>hH{Pw(GzT*KB2P?SNy5#|IqVXC085}%5bkZ z=A;bwieJ7;4<&$H@z=Y>Y{fB<;a>N%zjfDJ7hd@XK%RKF1SAl82QZ2oV-!U#tzRH*AZy-3Zxrbl zFw)Jxqb^a5BF{$wrKc!Ein1xq+I1OWO`K<>MM1n5Tr?7Pr>1rnX| zQ6OB(r&S`wrs79|8js)1;!00`U`BbFKpYUD_q>z$JoZo$A3bR)wu+z!y(|FVGGdY3G0y_W@O>m3Uv#e);-VS%*y0ayG}SF%%=Ze3Z6cl6H!!vfgs zWEe0Weil5OoqI>G%o`q%8vtM;cnqh)&j=w@Z3LV3fBLz^h=9C`ha*}DjR>IMZ{v>V=7s0eQ_1$FIZ<;GML5E`v6WV&3=kT8U8snU~{$6*8j&$WZBaI8I9`ZMgS6 zy(q6dpzy6*VnU`ofUVopqCzlT1!M2&XA>0x`I0S8K_OHTz)QBbb0D}JW1sv}@04Hx zxl+I(CS+Iu@uDV|!C)WF%zx>%6Qcui4S;1UWJU+D2EYj)%7)`qvitv~H&0YLIP?~7 zCE;}BG8nI8*o1%ScO=FHS znQgcX2HZF{_IHFf@dy{3|w>X;}UQd0wU>+}1Y8T6Xi zG}fY2Z=aYJkgrmM37KgDynsP>JTnv|pVQeWl9}${&>CYp;n1B^eDwwQp4pjF{lUa1 z0eLeLOvro^K**#8U50}D8Z+24J@wlXGaQ@%WM&W!wnZR=>)D^OMLqS$^FDQOf{^)? zaAxDs8bY|R{bj1Cmp(^Hei@MOc7qG4F9XOl`jJm#uXI5m?HhKrkKWh6ARu3w=O@Ms z+3*kbza$pgT>vIj7g86{^;~{JyohOi^@dL`3Mk?w0Wcx6D1es)vbhYX#TW@SGm?)! zc7O7%?ynW~c((^1_5h$kil^fCFGwzo&9vHF74E%+4e6`bO)Lq>#Uf67A+sca#UgnV zj&A8y`mKH8O-pT$0~0b!$>VT~1~TyOWvos=y-{MBgG1iEjBrF*@b2ZTdq0%5JRsNg zncTaV2e4qm%dfzJcduZnetP5N3dbbm-7Cl>xJ*J*p|&QFwvieA^`ZVX0V9jCIg9w8(5Ez^m`g^2xxq-nwZ25)F_apP$1k8Xnvo0fa;5@fLjA; zhuDda^!?58`V@Tv@zKaxjp|uT78;unq=pX+x(N zYRu9O2h#cl`6-4PL-h{_j2tnC#|ZCOK&0RPp}JOh%${iQ2%^UVGW`Y#&|?9l--vN) zj6CE&?DnC0t-}8}Dl|R(Cm>h)kb%m70$51X?W`Cz_#_)KRKKO?y z351h@m{^U01j0!Qz6kXM0rE7vI8-0h{IpXlC1|IeQYk?@O{Gqw3?T+r)Gh>+D*-iL zPT$E%u{Ve7|62-{nA9Ku{R{S_1`_%gXi}r~EJ&( z0N zkAiS8IuVGIKa{acBlY~kvY<2&n2;$8%9H~n(8_{HIdZrP9u8u6jnWf^gB%s|gh4@h z8z9a-VNeiDPRvIr1)ea3jToiZE*)Yw0ZcSuh}{H`Ko}B~7ivJF&>=xgpSc_X)UY5{ z4*q1vH@fw}huD%my7qXji!euAal<-zeuD(`FbA3(W>}D3z>lJfAi*vMI6C!`2t*yakosAPK_n1g&;X7r$RNhEwoL!Bc)SBaJKE!&N>Ii>K4^*aY^VTYd=N7% zod(7!;h)HE9j%v?Omq-TRAORKt{ET$go(jAt@+>p351Ek;&$dTx-=H29qrkvsiXB3 zO37>ogSNJ32W9dOAz;i7%JmUljC-(~CHN$9+*SAy!Zzfw3rv z6%^(BAkim_f-Rrtn>R%ABhE#^R-O3`L&>2d&PBmzo|S_f3DMFZ4?}mni?P$a>lNSannM?7*pwaTf!ADy|%=}dYzewE{s z^ltnrI}&AE+S*{+UKxq9A-Oh)?F7EO%%)KEE&InO`naa}4bX4x=mHX=-v;H>1QMX% z1~D}iplh-zaBN_oPsDrnZ{ls$!VNfz;nuZGQ*@=x5vh zIhM9Rn08LuKL?WggV-wP_Rk^v9}@P*Z-5@M?GF;7hiv-0}!cq3*G(Er5QM)BzqR^vuOF#nQD399ke2_pm8br|M z(|is{?Nm@X8@zd%f3XyNpx}X+>_GzMR8X#9K?3DeP)_zBG1;FAW}OX+$(|T|vi~)x zo(YP{Uh<$Q0kCws}lWPdtHlReF{x!95qrsZ6b#?3{s=YvL`Fm5gx_YxU*QsE`L zZeT*_l5JLyDB}{Dm2Up!QuF^{?Put>-t&j;o?sF;$geSaNCql@(9}#-NLbpnVA@bi zWPS-qUb`{85>$g;Z1^mF9DDdvB!0atX(Bv+^vxj4?wdSI8*ZiLFlm`Q6g%9q9Y2pOGm0!Tsc@8C zH!vYI$~Fo}lrf5oLf_xXql#Cs=Vt4T8&+6y7Z6P1TDrKT2@)z57G8ueqi+=^Ep5D& z_GN1MY~AbfkGHTRPJ5zBD!q#B{7i3KRAobf38*SdzG(vzP*oPXxjzNsBo#k}b(o_M zD4t@;^a4!8@+p=)&<06Dmi*cjNI*=n@U6T0?Ap?Zz zmJDMcfiT@d7`u%lfclh;oU3;z{?tLB!S|_yK!fj7OYUkW`QZE1LUKSsAW4C6HVb^N z=Qo^f$teg-sLZzH6a*5*%(m<)C`mP(XNfPGfB?cgr*Mjd^PIvd63(+^P7Gln&9jgb zXR$#2`0U*I4rW zOmfg;Yb<$wCRqSlW8wUauJGqmmwsy@MXr^6{H@OHjjw3M>QsU{^IJ=9t3w#N-&%;K z&vIQ*8`+w#^v=yTIx5tA8!fq&p3i%4qlK+>x*?uVy|)=#81XvEyIIdhTA00&(&L3- ziOClNP~U9HQz(!qW3z=N0ba6&I!wEbeU;K%HQr{44LV{%Wt){FKCA%}D%&hL(?v;T=@=)F$ zJCH~g0?E%jkkCsZ`Ly?N&{Fc_b(47eS8r4TLZtV2gzNMIkd zu%02`&!^P-uq8hdvQXC=9=1mS9zpT2Jpw>N@$iiikWU%)2{8iLwM978IpNfRf9b>? z0FVUG33~v51kec@01sfh2^Zskv(nD74vY2s{l8hb=7^P=2Uo*s!98vZ0IWlj?WHVh=1u_grxxC_G-VeBx3Sgt$~(Y&?Jj#1#u051lv!Dtg^w3zzD5lw5Zp z$Z4-z^4zZ`kB`@_fOz8zBp|L+E$&6@YFS#JP}(F`yiC6*+9!ljuz^B%Eoxr>5MEMR z;0OI`C3K zjbtBuqjxMG>6A*be`H8nrxs7VMuq}nVF;3RLWuoz$)XlTj|z768~vdU6%GQexhot5 z3Lq5@0tJu?LZAb!TGSVnA*C>0%syCx)PL$qy`NXA4&A*@e?Xa#8^kRhb@=NgGHMGd+aK2)n&d!NVEMwMNe^o?$lzOf+VO z-58K0WV;1OAj}}QKp+4DykaK%>l(dwyO|CG8GWXMK*h`q$t@eG0AXebA#NYduyrhL zPAF|HTf9c!8=MoeZvfPx=FAWAEy!jdfH>c76-Xe?w_61gi1Vpcqi6!CL**?={i|B9 za1||al&B#~LNXbJ5R{gL!*w6IM`m$&D+bsbT#qPIT0um7WL-MXONI>ilA@)-s0Evlfe@K3Fok(#{d4CAs zTt6iWfx;11XN&$@@e!vGx-xMjBrjdm<>opPlAl}$Njf2Xavl5UC=UsOBp~{PYl^$4AOX=Q+_V`_73z}3`?0DWNR#`8rRRbPm40FA zxgep^FKl~mT}l!Lg~e@c5U}SUr*O&=28HGPf~P@Z5Xgxb<+*8cy5F z8vLl=9U2zKHM2fs-g@v`tvsxZN%j8`uZNW4rwEvM0Pu1f93;TY!}9bOB*4qV*fynr z2@(ObJe(X8cAH_-g9brE3D4B(Z`Yt-PukbOD6jGvjFeo zRia|B!+f^}S>hnj=J=AZT!TXf2us4X#a=E*AS?;v zOW3O^z|^Brx-RwLetoU0WSxUS_hHsK7&JcDh2_O`=m29~7#Gu53Jh$IZ%I9RP+#q8 zx5dGr>o8lw@~EL6-yh!+mhahu1jd$dPQvVCVJU#uhyDns^-4YQi;f!o5%zfbl?DF# z(3P+f;h&>oLr&>hDMV?I3U5FB7I=Lp1ki8nl)Fos= zT?@+_(s(BavLN{zK9Hq3>ROoZXb~=Idi_SBk8qy{&CfyUDTJ?wv8EKj@cEuKV&phnb?$reb|YD5jRht`DksR8BezB9NcT<)ll3CbN63Nq!SLWdglsga}EXJ_y} z!DvT?fJQqi)b`P&f?Ewx0X3FI&+6?9$2uw$1I9Wk6a&VR3SB&_PcdLT>w8wet#G`f zLS`9X!z%_eWS}y>Mn)!o8>&9VxryxbS^e(9iH-`5@QF3#MgU}>GO-3W0-ookz+I=Z zm(J-gHlOOK(D0jDL*``l`S6=s1Ku)$u9n|oX$xwk9nEsZhnY#Xe}pHLvVPa$DGO>K zOYUBu%NMs=fOjG16|0Vocaly=Fmt&TvY@Kw4o7v zZH(TWDIgyl$-E7WcZx?k2(+FX=^&6xj*KLl$l8)ij*K*K!M}e{K$%+wThzd4S6tyB z@WWH5MdYCs5t-*g1qc-p@d6p@fcc@4H7_*oEUt7A=(0H0=Qs#-b~-0wi9Z|z6(G!s;AYnV;T?2#%CEba0xA$lI%sK_ya=sK}y0q7tbH zCQ3>S3+PhVLYCRgC@x;;AW&AeFd{Q5$N*tsB;HIq6)oKtMq0Ku|A!;s^m8fe*UWgP zc&URxr=LrmR?z9^(ugIt8446S{ahNkwUECT#;fUq`_tZDv(Bj8)3>)D7FM)%_N4nhbB>m7s; z5Y{`b2mxU|wW140z^6$!u?{Vbmy0(!2uSY-~Qp z5%49`t?W=sj z0LtD9lqgyF#^ni0Yg2f```XG_(Uyjti5)m6<#1Jc=KO*?rAZ>gXDA~x^B78fh z83@$!dPGj{Ac1~8A{VtFfqp%LMeQD8?;K10kPW`w*xT;I=#2{Ipn@MpZ_HD4sQqE| zMg?;K@nIAdM7FG`w_4iZC|;ERw4?EjD=;{UV^sbH`CDm1AI9Fl!{}Es%!URNwHp?d z9tjfA!=hPYCj=y*hefdwdyZ;%tH=UxyVE$|uH1p3Ew^&Jc8~!?c~rhy01^=8QEWON zqS`gEw5n*@C#mjt83$edswlD-e3U_LK$V-!y4+(t-)wSJ9+iLzsL9bBv4sE}@h6w~`a+Q}=l zv{})#FImOCMwx$B6uSU_MInV$ug_S+`;7l7`YbB5ATWXSSyZMPAc6E*6sZRP>uQBm zv3YF4ea4QWd5#X1J})X?se=r3=0&k<^(+c6vb0n*Z8KYXzwwNfqQWT(6`^o#VHDq@ zEPB9rvlPEX76v}B7TV>51lB?-pF9*KJal0+yeultl!?KQf)+*9rBQLFEP2pd9F=Fv zl7)5Xk|=F8P$Vs)rhF5{$JA>lS?+^IQ6UJs64an?qVixHk^uWAii2%BTqvRxZ8dxM zLF3NnS3{pqHef+gw26iqt|EBw!u!@}+Rmu>lm^X6TcaNSmmfX+o%d}~rM_5Ko_g5O5+KEu zcszu4wo$`x$GQ?PvQwD09x*y4w!@#v8pMRm_9zzf$WTCn+QE7}V%(eD5tY~Y!6Yt? ztqE{p4_;>79ZfsTetyIlAKo3s`@~qpdn|I{J;1H`CVM1ZFJwy|H6BcW!P8uN43IrE z4)Bd0kf_XFcFSYNQ;EG%xex{uGJB&~+F;~C2Go8w;W6XI#C~aXViGrsgReWd3@mk! zw%O0e7g3o6`1*ZRn^?Tg7J^71+Lu=Sy2#3F9*U-{_ji!xC9ev!u zOLo6fz~G124XIgYqGF331iI~v?VBLciZfBUOaY0?oQYzYvX*9~hSbK(sn`ButaFuI zw!;Ou2!)sJPzaKwqFEx}0||`FQGEA!w9YY5%d*s4Pa9+0C1o+`J`MTads$4b0YCzy zEGGX99Y|o5#W2CsG|`Z#!7;IG4+5nRj!72=3C+PV>B1nntnI=L$#NrOV%Hu7r*6~% zmc>IkTi_{>M#ki90TM_fV{qY0`iNU2OPd-?`z5vg86)ZrPL1IbIX7=3GVdobZr+w4 z0QpHw4t z!08u_+D$;)m7qSEACs2^APn94G3;f(#&tn0Krh6fp*Qd9u-Cd6oeIIQm8n}6#N;kP zBi=0wV%R01%ixWuR~EB9U5s0vUmTO4a{&`7i(>&113{vM#W8u-01`oOaZG;ymq;G; z7RTiGe4-s0Hp9r;S35iK5ewv>CcjP6hywn`RNxhi>?Ru>8`FYuV@z(VKnO}3V~9Qcfsw#Onr!O!TA z#1ezgk3YoJA7f&ElsxF|K$6M#vn31j*X7xhH0i<3_abJSfKT-$B5C4m!4^_2)symbCsnXpdtE1L+`*W15QaZGa=Ow6ifW z716@vNKAfqhJPO5D4X-TQIt4}QNgDdFfmAu#&C{_PqcXW_W+KwKVCN;NF0yJ7p}mB z%<&lVEgWY<2Gj}mQa9tF#EF=kg1{th3{%k4Tn3do#kO@b{+2iulT#Vq>xay#7^X6T zgI4~=?tcR~zs2P1O(DK>`CAM->Ua|oWufV3*n&5VmdP_QIc@n{yGmJorAq zhw-%0slb~?L%07!B=P+F8T|7Aec6NEjlU%N#$|iKgl^wB+KY`w58l?N9i)}rjrNKD zad`y2mzlZgRw*%mN~8%J9n;WB9NKvv$vcq}n6E^UGr>LD{Q z4x4o3GHCB$roC<4kr*78FRJ0~dB_ZoV?5GK$X}ix%DTR7+@2U3m$N09kQo}sY>D+C zWZ+oC*}AulClbTsa`pxjGQ;DTy|Lnh3{ZQN#@vxUW#L6pRXw0!29~+<7qS4-T!CIx5meD zy9Rrj9=vOhy8v;erueI&AYp3I;u~ThfiZ#FP2Uj18}+mm+v{E9zQjbkE?`1tBGu*J zTn5w>cKltVG%>~Q&l*MwPv_x<0G`2uM04iF<-79CQbd#EpE>-A$;wkBjcE=x1n!$L-F>6Tlz0I~ye6k5gyU@qQCB`$^Wc zpK(v&Nk@gc^JHAE8zBRglX0xuZsRKG&fnOKenyA(zu9eS!Y?lTX156>5PplxeJ_we z_$`jJ=C2eEf&1d;NFaTfEjt(_kUq>t2jlLa+SJlM%1)br{=JR8 zHVni4!!n~m;G=A#mgw20)UyNG@-kym;lONZColmyFk6mbkboSRjZS?B=50#GAIgRf zG~O*7>Znks4$YQ-8VoW}8Jdk7(X>O}l*Z@?)@+dRWZ{Txxvv2xR7PaWPl|zr%7|>F zMVJeq0;-&)1{t>(l{=--kSx!ZpVxs531uUs(|Br1MjxFm#uEtGdvvz6H%MrX&X(f| zBs527+vBMz*?YXOH+y3U#?yG)+<3&FNpph)(0JS2AOSR<%>8$myBR+El$}-$b3c@1 z|K8zAuJXEAb~uUtX_b|H6vqv!cGn~ z8WeuwsF2k^$(BP2GEn&>8$;Nt1|Dgs&X3c_R%@?Q|FHtpWHb~a&{A%adJ9njD zFLmzTf{2CZV&+KvxlAgv|G|Ai8T=9acTJ8eJPU11ywtj{)*!rz;dzvH_!4Dn@) zZ67cJwZ-;ykbv4ko=$GtoE-FfHes~!aPjwcWxzybzPBp_l7zD5CpJL>;rndlN_2kH zoWjixc59{aOvw%hfo!!STfRsL86fP)t|QKnKmuV$w!DeeoT#5fC}I054Xxo%b|k?g zDE?$y6eJh5BS~}e)O|9NWR5ko`}R3Cpe>1gP8%pB?XwLEMF8z1gMM8D25o^`3pr_@ z=DOmY@R6*SyLRt^YeM_KGa{*X#u_8d;IJG!8EZi{9+@NdfIy&-kvY;8L86e6IWm-k zL?I({VCwbs=1~hW!pYRJX~xEIi<3DrSb|IJoLB)*mq0@4WDZ^^;g?xjz`WY;Im+c6 zHU6cW{x0+M)X2p~)Kv?>TY=fW!#+JtymOju-*N;3MFCj(RCa zi~z}l-h~`F0wfC~;9?GqfC(Yq1kCp1bCs%GHC3?G80YN_LX77Y{O0EPTp43PqEh2? zM6g_6-!Y}?q1FfZ{&CX3* z!{)6sjt6Jw+V@Lak`v6!6;1#Gi1Tt~Z-4~iyj=OX4@zr0lC>W zBuGGRCPU7l#IqF)sr`_v>`HCkY!tal@ly;3Jb>O0xpFvwgx(LiayWp5=lzhIwF{m{ zTQS7o!(m6R`ctkL4w47Gow;&2NEU{}kGb}Q(27Fe&$%~E2;jiJV2KFJ-4u^ti$k_~K?3NIZC;RE6XvBiK3ZGa(cHAEge!i3lC|II ziL&JPMsb~^xtJK}uQIg8J)IM|%5S+Uedb38E$-avTr|3e5$7s>zWyHc&m%zxyvt3Md68~M!}fZR(D{SB{<@DVAySP>C>5!f zcNo_^o$*t|sn*=GqY^SsfdtH`gp5-l0W&Hg;}l4E%cw-HiiC($#1L_+JR##$YZ@h$ z2_C1|zCD<}DigB*@d)6R2^pq90=zO|kFVAgrY0mrn9AIXFg3xcC55R8PAw@+O>k;S zVQNAG{m4cT=an|Z{TTC~p5p&nY7kjXD(ptCN4l;W3nVC8FYwd4q)Vt_03e=&Z2~4f8z- zwSK&XQ0{TUC@cg+{3UxlhVq_7NF3yVgz}yQF7D9%!8Vi!?q%PdFm7wIH-SIcKu3{a zLS=6v(b^`VvNuuOj{Z(3o?-)Se?n~@zg?8~*h!;x6EH-a!DA@zPsAEZW!k^qpJ>vI zUvX?hJ8K6?xsg!bdeW%Z5DZ(HwyqC4%CvQTkd%8<=x%HMe^b{Q8`W`z*SEXdJ5HO0 zHSuF@&lwy$q2ZAhf*tJji>Pg?hP3@bn@XutrK*&uQ9&)gT1Dn1VFOWY{5W9jM5%Cs z!59OD#iW%2;ZZv#O=;A$QF)|wh(v(6sKyZ0cE2-sZnyvTeE00!bLV{L%-lP3&y8Qx z`0l}in?5LTO=B@E4U1|(t@w5A-Q=3j{F)~G3SQ3zj@GX0MBp}vXzjYjqO*+q5D`z; zb?PSKN#4p7z^*>r(AWhip9ygY_;f>~Zli3-bC4`Z+|YIPxxcdK$7yb9uQK(5`N4BM z`I)~i4KaxE8oRD`OS@tk5UsMSo(B;&-O|`EEIUAJB)v>>--O@LFqv&Z=rI|_A9T>d zq;@wa5YfV<#?8sNvP~e_G?VvIrs7}zKch|f85$yKyqR3Gsj(z(rvEh~ByJ`&6LgLw zn`WYyC>WZc9t=$_6x~k2(Bv@uZ!5W>$w3&Jwvh}?SMHHVrKl9zPLbDn$8G;|qMc&J zcJpz*WP}c0KZPgr9pp-80Yn#c5S|7|b#_$yl&1k>Q~u++lRUc>P(bK(61D<1S{+DT zy!4L$nC>F)A>kc#q$|4!b60tyn=hTj+_gPDh?pGktBT?7V^w0(U20z2jHv zo#feI6A(H(>EW-+W;=-J!(Ci`*RRpLvgeW~)w|?efjtPwZf?H|d%Cmdk|)*OaxPF7 z?I7-U^S|%yWGOQ7LR{?GlVsE0foPXa=xhfpl>&rz#{fa=Sf z``lmcQD4>~c~;#g7JV0SlTOC_DRPKwrv2|E`w3G=dE%Cqu-Q-E0q&Xh>zeQ@2twfK z^!?;Ms16Z*x1aE2SeOA3%-B!m2fz#o8v$(C43G;OhrogXa$(~j37Y{z*vJd&Y4Pn4 zc@?Qf^Nlt9!x{fubr1u#;vjl@C}b-{tL(PbK!i<0gf)iW2*FUk^eCYe*p(UowZu`1 zX-lvIA=oi)odv;;g`Er#h8>emmgN8xAC+x~f1ky2K!!ZKi(f$KWC&3y#IOZPk9U4W zRqEr}W`tkIr5W5$K?U{S?M5QFd3i`FIMpHk!w<)m7246ivF z%jfrV)vxD#N~m4YCN~##!s3a{GjL_j>IrfaC!jd>1i5Nl5OL}W!jw}+aEQnq6O;@- zVI(4EidG{po%=!1EdD=5-jk_6@bt(21DP9NQn+;i65P7f*=w63_h1(y{ys%`UKpG} zHm*-m-YV-%9F-FYGVcj{tq#GpPSLW^3aezBssDEFDG6LuOr564W|NaTf-0ZS|B}1! zRmYRl;E7tbP&uN`S){F zz20bCO-z8$*=RsY)C_|TqIDCe=BgF6$+%@)fY8}wu#DRP|JNtuTTEm-|6;DHPi`^T zeJL4yKPf{&Oyli6SHp>=? zk6QU7fV$PVv<48Zw;Ci6=?Pm}u#FezsTI1-c#qml1Q0rH2CH#0B%&~&tQBm|Q+0ZW zaoGx>pa$7W7K)3+GwpmOPp#DL#;bJgh-cc(ifZebBJoTIH_U@)I*iNc0HM=ikkMs< z-B#`E2 zBiBLe?o&@ByA2l5#E%W)#~$Oo68I4!+JqeJ=!6JA_L#EZJ^~`HuE#7~Wc>&ce(W)e zUvYjE!1}Q_^rJ&yQE%u+2ZDiFFeaLvhOaM!u&^cuAYC>Q;w#ppl-W2-ju<`6G z6gZdE*F9|T3PsQkUcxxSAEF&PlD#geu6x8(r*nS-#x^G7858*(mlUgKlNp1ep~BNf z;pwpP-V~muSL0VuQvo>KJZwsXx_uDgtzpSsl4&4dYy2T+d"; the delivery endpoint verifies it with the Exchange's +// published public key. No shared secret (separate concern). // // Retrieval-URL identity binding (OPTIONAL, DPoP-style — RFC 9449): // The Exchange MAY bind a signed retrieval_endpoint to the requesting agent // by embedding agent_identity_hash — the RFC 7638 JWK Thumbprint (SHA-256) -// of the agent's Ed25519 request-signing key — inside the HMAC-signed URL, -// and echoing it in the response. A capable delivery endpoint (edge function) -// verifies the binding fully offline: confirm the URL HMAC (proves the hash -// is Exchange-issued and untampered), then require the fetcher to present its -// public key and an RFC 9421 signature over the retrieval request, and check +// of the agent's Ed25519 request-signing key — as the URL's agent_id query +// parameter, which the URL signature covers, and echoing it in the response. +// A capable delivery endpoint (edge function) verifies the binding fully +// offline: verify the URL's Ed25519 signature against the Exchange's +// published public key (proves the hash is Exchange-issued and untampered), +// then require the fetcher to present its public key and an RFC 9421 +// signature over the retrieval request, and check // thumbprint(presented key) == agent_identity_hash. No JWKS fetch required. // Enforcement is NOT mandatory: a bearer-only signed-URL CDN that cannot run -// code falls back to HMAC + short TTL + TLS. RAMP reference implementations -// run on edge functions and DO enforce it. +// code falls back to the URL signature + short TTL + TLS. RAMP reference +// implementations run on edge functions and DO enforce it. // ============================================================================ // ============================================================================ @@ -3501,11 +3505,16 @@ message DomainVerificationConfirmation { // Optional: signing key to register upon successful verification. // If present, the key is registered atomically with verification. - // Key format depends on CDN type (PEM for CloudFront, hex for HMAC). + // Format follows cdn_type: a PEM-encoded RSA public key for "cloudfront", or + // the base64url-encoded raw Ed25519 public key (the JWK "x" value) for + // "edge-ed25519". optional string signing_key = 4; - // CDN type this key is for. - optional string cdn_type = 5; // "cloudfront", "akamai", "fastly", "hmac" + // Which delivery-URL verification scheme this key is for: "edge-ed25519" (a + // code-capable edge that verifies the Ed25519 URL signature itself) or + // "cloudfront" (AWS CloudFront trusted key groups, RSA, verified natively by + // the CDN). Mirrors the Exchange-side tenant signing scheme. + optional string cdn_type = 5; // REQUIRED. Bare host of the recipient this request is addressed to (e.g. // "exchange.example" or "exchange.example:8081"). See "Request recipient" in From d7079e11ed7931d9b8aa525b3bfb165b7e0bd1ff Mon Sep 17 00:00:00 2001 From: noxlesh Date: Tue, 1 Sep 2026 17:32:06 +0300 Subject: [PATCH 02/16] docs(edge): rewrite signed-URL verification for the Ed25519 scheme This page specified a scheme that does not exist, in enough detail that an implementer could build it and be confident. It described HMAC-SHA256 over a secret shared with the CDN, a canonical string of four fields joined by "\n", a hex digest, a timing-safe comparison, and a max-TTL bound. None of that is in the code. Four of the five things a verifier needs were wrong at once: the algorithm, the signed input, the signature encoding, and the parameter names. What ships: a detached Ed25519 signature over "GET\n", base64url with no padding, verified with the Exchange's published public key. The canonical URL is the whole URL with sig removed and the query sorted, not four selected fields, so scheme, host, path and every publisher query parameter are covered too. The expiry parameter is exp, not expires. There is a kid parameter, which the page never mentioned. There is no txn_id parameter at all -- the join key for reconciliation is the signed-URL hash, recorded by both the Exchange and the edge. The "two verification modes" framing is the source of the error and is gone. There is one signing scheme with two deployment postures: a code-capable edge that verifies Ed25519 and can enforce agent binding, and CloudFront verifying RSA natively but unable to run the binding check, so bearer-only. Deletions. Timing-Safe Comparison goes because there is no secret, no expected value and no comparison -- crypto.subtle.verify is a verification, not an equality test, and the section's premise does not exist. The max-TTL check goes because the verifier does not implement one; expiry is the only time check. The Akamai target goes: EdgeAuth is a symmetric token scheme built on a shared secret, which is the model this correction exists to remove, so documenting it as supported would reproduce the defect. Additions, all previously undocumented and all load-bearing for anyone writing a verifier: verification fails closed on a key-resolution error; the signature is checked before the expiry, because both come from the same untrusted URL; the full refusal vocabulary for the URL check and the proof-of-possession check; the proof-of-possession wire contract, including that its signature parameters are ordered keyid, alg, created, expires and that its Signature header uses standard base64 while the URL's sig uses base64url; that binding enforcement defaults on and only the literal string false disables it; that directory keys carry no identifier, so kid is a locally recomputed thumbprint rather than anything a publisher asserts; and that a custodially registered agent does not fetch its own URL, because its key never leaves the registry. Single-use enforcement is marked as not implemented rather than described as available, and its KV key is rebased on sig now that txn_id does not exist. --- .../edge-function/signed-url-verification.mdx | 354 ++++++++++-------- 1 file changed, 200 insertions(+), 154 deletions(-) diff --git a/website/src/content/docs/components/edge-function/signed-url-verification.mdx b/website/src/content/docs/components/edge-function/signed-url-verification.mdx index 4757a9d6..2dac7f21 100644 --- a/website/src/content/docs/components/edge-function/signed-url-verification.mdx +++ b/website/src/content/docs/components/edge-function/signed-url-verification.mdx @@ -1,202 +1,289 @@ --- title: "Signed URL Verification" -description: "HMAC-SHA256 verification, URL parameter parsing, TTL checking, agent identity binding, and timing-safe comparison" +description: "Ed25519 signed-URL verification: the canonical message, URL parameters, expiry, method binding, agent-binding proof of possession, and the refusal vocabulary" --- -The Edge Function verifies signed URLs to gate access to protected content. Two verification modes are available, chosen per deployment: CDN-native verification and custom HMAC verification. +The Edge Function verifies signed URLs to gate access to protected content. -## Verification Modes +There is **one signing scheme and two deployment postures**. A code-capable edge — Cloudflare Workers, Fastly Compute, AWS Lambda@Edge — verifies an Ed25519 signature itself and can enforce agent binding. AWS CloudFront verifies an RSA signature natively, before any function code runs, but cannot run the binding check, so that path is bearer-only. Which one a publisher gets is selected per tenant by the Exchange's `signing_scheme`, whose values are `ED25519` and `AWS_CLOUDFRONT_RSA`. -### Mode A: CDN-Native Verification (CloudFront, Akamai) +In both postures the delivery endpoint holds **public keys only**. Nothing secret is shared between the Exchange and the CDN. -The CDN platform handles signature verification at the infrastructure level, before the edge function code runs. +## The Signed URL -```mermaid -sequenceDiagram - participant Agent as AI Agent - participant CDN as CDN Infrastructure - participant Edge as Edge Function - participant Origin as Origin Server +``` +https://cdn.provider.example/premium/article.html + ?agent_id=NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs + &exp=1773451434 + &kid=vJ3xR1mQ7nT2aB8kL0pY5wZ6cE4dF9gH1jK3lM7nO2p + &sig=k7Qm2xR9vT4nB8aL... +``` - Agent->>CDN: GET /premium/article.html?Signature=...&Key-Pair-Id=...&Expires=... +| Parameter | Meaning | +|---|---| +| `agent_id` | RFC 7638 JWK Thumbprint of the **agent's** request-signing key. Optional — when it is absent the URL is a bearer credential. | +| `exp` | Expiry, Unix seconds. | +| `kid` | RFC 7638 JWK Thumbprint of the **tenant's** Ed25519 public key. The edge resolves it against the Exchange's key directory. | +| `sig` | The Ed25519 signature, **base64url with no padding**. | - Note over CDN: CDN infrastructure verifies signature
against trusted_key_groups (CloudFront)
or EdgeAuth token (Akamai) +Four properties of this URL are load-bearing and easy to get wrong: + +- **Parameters are sorted lexicographically by key** and encoded exactly as Go's `url.Values.Encode()` produces them. The Go, TypeScript and Python SDKs reproduce that encoding byte for byte, pinned by shared vectors. +- **Publisher query parameters carried over from the resource URI are preserved, and they are covered.** The signature covers everything on the URL except `sig` itself: scheme, host, path, and every query parameter. +- **Scheme, host and path are signed verbatim, byte for byte.** No host lowercasing, no default-port stripping, no path re-escaping. A verifier that normalizes the URL rebuilds a message the signer never produced. +- **The signature covers no HTTP method, header or body.** Method binding comes from the proof-of-possession check alone — see [Method Binding](#method-binding). + +The edge strips `agent_id`, `exp`, `kid` and `sig` before forwarding to origin, on both the signed and the unsigned path, so an origin can never receive attacker-injected attribution parameters. + +There is **no transaction-id parameter**. Reconciliation joins on the signed-URL hash — SHA-256 of the URL verbatim — which the Exchange records on the transaction and the edge records on its delivery event. + +## The Canonical Message - alt Signature invalid or expired - CDN-->>Agent: 403 Forbidden (CDN-generated) - else Signature valid - CDN->>Edge: Forward request (signature already verified) - Note over Edge: Agent identity binding check
Single-use enforcement
(if enabled) - Edge->>Origin: Forward to origin - Origin-->>Agent: 200 OK + content - end +``` +GET\n ``` -**CloudFront implementation**: Configure a `trusted_key_groups` reference on the cache behavior for `/premium/*`. Upload the RSA public key to CloudFront Key Management. CloudFront verifies the `Signature`, `Key-Pair-Id`, and `Expires` (or canned/custom `Policy`) parameters before the request reaches any function code. +The literal ASCII `GET`, one newline, then the canonical URL. Building it: -**Akamai implementation**: Configure EdgeAuth token verification on the property manager for protected paths. The EdgeAuth token includes a hash, expiry, and optional IP binding. Verification happens at the property level. +1. Split the raw URL at its **first** `?`. Keep the prefix — scheme, host, path — verbatim. +2. Parse the query into pairs. +3. **Drop `sig`.** +4. Sort the remaining pairs by key and re-encode. +5. Rejoin prefix and query. -The Edge Function does not verify the cryptographic signature in this mode. It only performs additional checks (agent binding, single-use) that the CDN cannot do natively. +Sign the UTF-8 bytes of that message with Ed25519 and encode the signature as base64url without padding. -### Mode B: Custom HMAC Verification (Cloudflare, Fastly) +Signer and verifier must agree on these bytes exactly. One consequence worth stating, because it bites in test environments: if a deployment rewrites the URL's authority for port routing, it must still send a `Host` header matching the authority that was originally signed. -The edge function code verifies the signature using HMAC-SHA256. +## Verification Order ```mermaid sequenceDiagram - participant Agent as AI Agent - participant Edge as Edge Function (Worker/Compute) + participant Agent as Fetcher (agent or registry) + participant Edge as Edge Function (Workers / Compute / Lambda@Edge) + participant Dir as Exchange key directory participant Origin as Origin Server - Agent->>Edge: GET /premium/article.html?sig=abc...&expires=1773451434&agent_id=&txn_id=txn-mp-93a7f2 + Agent->>Edge: GET /premium/article.html?agent_id=&exp=1773451434&kid=&sig=... - Note over Edge: 1. Extract sig, expires, agent_id, txn_id from URL
2. Reconstruct base URL
3. Canonicalize: baseURL\nexpires\nagent_id\ntxn_id
4. HMAC-SHA256(canonical_string, secret)
5. Timing-safe compare expected vs provided
6. Check expiry + Note over Edge: 1. No sig parameter? bot gate, then origin
2. Resolve kid to a public key
3. Rebuild "GET\n<canonical URL>"
4. Ed25519 verify, then check exp
5. Method gate
6. Proof of possession, if agent_id is present - alt Verification fails - Edge-->>Agent: 403 Forbidden - else Verification passes - Note over Edge: Agent identity binding check
Single-use enforcement
(if enabled) - Edge->>Origin: Forward to origin + Edge->>Dir: fetch public keys (cached; skipped when keys are pinned in config) + Dir-->>Edge: JWK Set + + alt Key resolution fails + Edge-->>Agent: 503 verify_unavailable + else Signature invalid or expired + Edge-->>Agent: 403 + reason + else Signed non-read method with no binding to run + Edge-->>Agent: 405 method_not_bound + else All checks pass + Note over Edge: strip agent_id, exp, kid, sig + Edge->>Origin: forward Origin-->>Agent: 200 OK + content end ``` -## URL Parameter Parsing +Two behaviours here are deliberate and neither is obvious. -**HMAC signed URL format** (current implementation): +**Verification fails closed.** If the key-resolution leg throws — the directory is unreachable, the response is malformed — the edge answers `503 verify_unavailable`. It never falls through to the origin. -``` -https://cdn.provider.com/premium/article.html?expires=1773451434&sig=a7f3b2c1... -``` +**The signature is checked before the expiry.** Both `exp` and `sig` come from the same untrusted URL. Reading `exp` first would mean acting on attacker-supplied bytes before anything had proven they were Exchange-issued. -- `expires` -- Unix timestamp (seconds since epoch). -- `sig` -- HMAC-SHA256 hex digest of `{baseURL}{expires}` using the shared secret. +Refusals from the URL check: -**Production enhancement** -- add delimiter and agent binding: +| Reason | Meaning | +|---|---| +| `missing_sig` | No `sig` parameter on a request that reached the verifier. | +| `missing_exp` | No `exp` parameter. | +| `bad_sig_encoding` | `sig` is not valid base64url. | +| `bad_exp_encoding` | `exp` is not an integer. | +| `expired` | `exp` is in the past. | +| `bad_agent_encoding` | `agent_id` is not valid base64url. | +| `signature_mismatch` | The signature does not verify, or `kid` resolved to no key. | -``` -https://cdn.provider.com/premium/article.html - ?expires=1773451434 - &agent_id=NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs - &txn_id=txn-mp-93a7f2 - &sig=HMAC-SHA256(baseURL\nexpires\nagent_id\ntxn_id, secret) -``` +Refusals from the edge itself: `method_not_bound` (405), `ai_bot` (403), `verify_unavailable` (503), `origin_fetch_failed` (502), and `unknown` (403) when a result carries no reason. Every rejection uses the same JSON body shape, `{error, reason}`, and the `reason` matches the structured log record. -- `agent_id` -- the agent's `agent_identity_hash` from `TransactionResponse`: the RFC 7638 JWK Thumbprint (SHA-256, base64url) of the agent's Ed25519 request-signing key. Because it is part of the HMAC input, a URL holder cannot swap the Exchange-issued value. -- `txn_id` -- transaction ID for reconciliation and single-use enforcement. -- `\n` delimiter between fields prevents canonicalization ambiguity (as noted in the threat model's HMAC implementation note). +Cost on a capable edge: one Ed25519 verify at roughly 50-100 µs, plus a SHA-256 thumbprint under 10 µs. No socket, and well inside every edge runtime's CPU budget. -## HMAC Canonicalization +## Expiry -Both the Exchange (signer) and the edge function (verifier) MUST use the same canonical format for HMAC input. Fields are concatenated with `\n` delimiters in this fixed order: +The edge rejects a URL whose `exp` has passed, using its own clock. That is the only time check the verifier performs. -``` -baseURL\nexpires\nagent_id\ntxn_id -``` +The Exchange chooses the lifetime when it signs; the reference deployment uses five minutes. Because `exp` is covered by the signature, a URL holder cannot extend it. -Where: -- `baseURL` -- the content URL without query parameters (e.g., `https://cdn.example.com/premium/article.html`) -- `expires` -- Unix timestamp in seconds (string representation) -- `agent_id` -- the agent's RFC 7638 JWK Thumbprint (matches `TransactionResponse.agent_identity_hash`) -- `txn_id` -- the transaction ID (ULID) +## Method Binding -All four fields are REQUIRED in production signed URLs. The `agent_id` field binds the URL to a specific agent (preventing sharing, Threat T8). The `txn_id` field enables three-sided reconciliation (CDN logs, Exchange transactions, Usage reports). +The URL signature covers no HTTP method. A signed request with a method other than `GET` or `HEAD` is therefore refused with `405 method_not_bound`, **unless** the proof-of-possession check will run — that check covers `@method`, and it is the only thing that binds one. -This canonicalization format MUST match the Exchange's signing format. Any mismatch between signer and verifier will cause all signed URLs to be rejected. +## Agent Identity Binding -## TTL Checking +The signed URL can be bound to the agent that purchased the resource, so a leaked URL is useless to anyone else. The binding follows the **DPoP pattern ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449))**: the Exchange embeds the agent's key thumbprint into the URL it signs, and a capable edge function enforces proof of possession at fetch time — **with no outbound network call**. -Signed URLs include an expiry timestamp. The edge function enforces two checks: +**Definition.** `agent_identity_hash` (URL parameter `agent_id`) is the **[RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) JWK Thumbprint (SHA-256, base64url)** of the agent's Ed25519 request-signing key — the same key published in the agent's directory at `{domain}/.well-known/http-message-signatures-directory` and used for RFC 9421 request signatures. RFC 7638 defines one canonical JSON form and one hash, so signer and verifier compute the identical value. It is present whenever a bound `retrieval_endpoint` is returned, and it is covered by the URL signature. -1. **Expiry check** -- reject URLs where `expires` < current time. -2. **Max TTL check** -- reject URLs where `expires` is more than `maxUrlTtlSeconds` from the current time (prevents URLs signed too far in the future). +:::caution +This is **not** a hash of an account identifier. An identifier is not a secret, so hashing it binds nothing an attacker cannot reproduce. The thumbprint binds the URL to a key whose *private* half the fetcher must prove it holds. +::: -The default max TTL is 300 seconds (5 minutes). This limits the replay window even without single-use enforcement. +### Who fetches -## Timing-Safe Comparison +Two deployments, one implementation. -HMAC comparisons use constant-time comparison (XOR loop), never `===`. This prevents timing side-channel attacks where an attacker could determine partial signature matches based on response timing. +An agent that holds its own key fetches the URL itself through the SDK, which mints the proof of possession from that key. -| Operation | Time | -|---|---| -| HMAC-SHA256 computation (Web Crypto) | 0.1-0.5ms | -| Timing-safe comparison (64-byte hex) | < 0.01ms | +An agent registered through a custodial registry does **not** fetch. Its private key is held in the registry's custody and never reaches it, so an agent-side fetch could only ever be refused — it cannot prove possession of a key it does not have. The registry performs the fetch on the agent's behalf, presenting the same key it signed the offer acceptance with, and returns the bytes. Both paths run the same SDK fetcher; they differ only in whose key is used and which process runs the call. -## Agent Identity Binding +### What the fetcher presents -The signed URL is bound to the agent that purchased the resource, so a leaked URL is useless to anyone else. The binding follows the **DPoP pattern ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449))**: the Exchange embeds the agent's key thumbprint into the URL it HMAC-signs, and a capable edge function enforces proof-of-possession at fetch time — **with no outbound network call**. +1. Its **public key**, in the `X-RAMP-Agent-Key` header — raw Ed25519 key, base64url with no padding. The choice of carrier is security-irrelevant, because the thumbprint is locked by the URL signature. +2. An **RFC 9421 HTTP Message Signature** over the retrieval request, covering exactly `@method` and `@target-uri`. -**Definition.** `agent_identity_hash` (URL parameter `agent_id`) is the **[RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) JWK Thumbprint (SHA-256, base64url)** of the agent's Ed25519 request-signing key — the same key published in the agent's WBA directory at `{domain}/.well-known/http-message-signatures-directory` and used for RFC 9421 request signatures. RFC 7638 defines one canonical JSON form and one hash, so signer and verifier compute the identical value. It is present whenever a signed `retrieval_endpoint` is returned, and it is covered by the URL HMAC. +The signature parameters are a wire contract, in this order: -:::caution -This is **not** a hash of an account identifier such as `billing_ref` — an identifier is not a secret, so hashing it binds nothing an attacker cannot reproduce. The thumbprint binds the URL to a key whose *private* half the fetcher must prove it holds. -::: +``` +Signature-Input: sig1=("@method" "@target-uri");keyid=...;alg="ed25519";created=...;expires=... +Signature: sig1=:: +``` -**What the agent presents at fetch:** +Two details a verifier will not forgive. The order `keyid;alg;created;expires` is fixed — a generic RFC 9421 emitter produces a different order and its signature base will not match. And `Signature` uses **standard** base64, while the URL's `sig` uses base64url without padding; the two encodings are not interchangeable. -1. Its **public key** — in a header or a query parameter; the choice is security-irrelevant because the thumbprint is HMAC-locked (see below). -2. An **RFC 9421 HTTP Message Signature** over the retrieval request (covering `@target-uri` + `created`), proving possession of the corresponding private key. +`@target-uri` is signed as the URL string **verbatim**. Routing it through a parsed URL decodes percent-escapes, and `%2F` then becomes a real separator — producing a proof that cannot verify and a 403 that says nothing about the URL having been the cause. -**What the edge function checks -- fully offline:** +### What the edge checks, fully offline -1. Recompute the URL HMAC with the shared secret → proves `agent_id` is Exchange-issued and untampered. -2. Check `expires` (local clock only). +1. Verify the URL's Ed25519 signature against the Exchange's published public key. This proves `agent_id` is Exchange-issued and untampered. +2. Check `exp`, local clock only. 3. `thumbprint(presented public key) == agent_id`. -4. Verify the RFC 9421 signature with the presented key (proof-of-possession). +4. Verify the RFC 9421 signature with the presented key, and reject a `created` more than 300 seconds ahead of the edge's own clock. -All four pass → serve. No JWKS fetch is required: the Exchange already authenticated the key at transaction time and froze its thumbprint into the HMAC; the edge inherits that authentication through the HMAC it can verify locally. +All four pass, and the request is served. No key fetch is required for the agent: the Exchange already authenticated that key at transaction time and froze its thumbprint into a signature the edge can verify locally. ```mermaid sequenceDiagram - participant Agent as AI Agent + participant Agent as Fetcher participant MP as Exchange participant Edge as Edge Function Agent->>MP: ExecuteTransaction (signed, RFC 9421) - Note over MP: T = RFC 7638 thumbprint(agent key)
embed T as agent_id, HMAC the whole URL - MP-->>Agent: retrieval_endpoint (?agent_id=T&sig=...)
+ agent_identity_hash = T + Note over MP: T = RFC 7638 thumbprint(agent key)
embed T as agent_id, Ed25519-sign the whole URL + MP-->>Agent: retrieval_endpoint (?agent_id=T&kid=...&sig=...)
+ agent_identity_hash = T - Agent->>Edge: GET retrieval_endpoint
present public key + RFC 9421 signature - Note over Edge: 1. Recompute URL HMAC (T untampered)
2. Check expiry
3. thumbprint(presented key) == T
4. Verify RFC 9421 sig (proof-of-possession) + Agent->>Edge: GET retrieval_endpoint
X-RAMP-Agent-Key + Signature-Input + Signature + Note over Edge: 1. Ed25519-verify the URL (T untampered)
2. Check exp
3. thumbprint(presented key) == T
4. Verify the RFC 9421 proof alt All four pass Edge-->>Agent: 200 OK + content else Any check fails - Edge-->>Agent: 403 Forbidden + Edge-->>Agent: 403 + reason end ``` -**Why a stolen URL is harmless.** To pass step 3 the attacker must present the agent's *public* key; to pass step 4 they must hold its *private* key. They can do one or the other, never both. Rewriting `agent_id` to match their own key fails step 1 (they do not hold the HMAC secret). The defense is `(HMAC-locked thumbprint) ∧ (proof-of-possession)`, not key secrecy — which is why the public key may travel in the clear. +**Why a stolen URL is harmless.** To pass step 3 the attacker must present the agent's *public* key; to pass step 4 they must hold its *private* key. They can do one or the other, never both. Rewriting `agent_id` to match their own key fails step 1, because they cannot forge the Exchange's signature. The defence is a signature-locked thumbprint combined with proof of possession, not key secrecy — which is why the public key may travel in the clear. | Attacker with a stolen URL tries to… | Fails at | |---|---| | present their own key + sign with their own private key | step 3 — `thumbprint(their key) ≠ agent_id` | -| rewrite `agent_id` to match their own key | step 1 — HMAC fails; they lack the shared secret | +| rewrite `agent_id` to match their own key | step 1 — they cannot forge the Exchange's Ed25519 signature | | present the agent's public key (it is public) | step 4 — they cannot produce the RFC 9421 signature | -**Enforcement is OPTIONAL -- capability depends on the delivery node:** +Refusals from the proof-of-possession check: `missing_agent_key`, `bad_agent_key`, `missing_sig`, `malformed_sig_input`, `unsupported_alg`, `bad_covered_components`, `keyid_mismatch`, `thumbprint_mismatch`, `pop_missing_created`, `pop_future_created`, `pop_missing_exp`, `pop_expired`, `pop_sig_invalid`. + +**Enforcement depends on what the delivery node can do, and defaults on where it can.** -| Delivery node | Enforces binding? | Behavior | +| Delivery node | Enforces binding? | Behaviour | |---|---|---| -| Edge function (Cloudflare Workers, Lambda@Edge, Fastly Compute) | Yes | Full steps 1-4. RAMP reference implementations run here and **do** enforce. | -| Bearer-only signed-URL CDN (CloudFront / S3 presigned, native Fastly) | No | Validates its own HMAC + expiry only; falls back to **HMAC + short TTL + TLS**. | +| Edge function (Cloudflare Workers, Fastly Compute, Lambda@Edge) | Yes, by default | Full steps 1-4. RAMP reference implementations run here. | +| CloudFront-native, or any bearer-only signed-URL CDN | No | Validates its own signature and expiry only; falls back to the URL signature + short TTL + TLS. | + +On a capable edge the check is on unless it is explicitly turned off: the configuration value must be the literal string `false` to disable it, and any other value — including an absent one — enforces. Turning it off is a downgrade to bearer security, and it is the reason the method gate above exists. + +Because the RFC 9421 signature covers `@method` and `@target-uri`, a captured fetch signature cannot be replayed against a different URL, or outside its short window. + +## Key Distribution + +**The edge function verifies, it does not sign.** It holds public keys only, in both postures. + +| Posture | Who signs | Who verifies | Edge function holds | +|---|---|---|---| +| Edge Ed25519 | Exchange (private key) | Edge function (public key) | The public key | +| CloudFront RSA | Exchange (private key) | CloudFront infrastructure (public key) | Nothing — the CDN verifies | + +Because verification needs no secret, compromising an edge does not let an attacker forge URLs, and rotation is one-sided: publish the new public key, and the edge picks it up. Nothing has to be updated on two sides at the same moment. + +The edge resolves `kid` to a key from the Exchange's key directory at `/.well-known/http-message-signatures-directory`, cached with a one-hour TTL and a single in-flight fetch. Keys may instead be pinned in configuration, in which case a covering `kid` skips the fetch entirely. + +**Directory keys carry no key identifier of their own.** The edge computes the map key locally as the RFC 7638 thumbprint of the key material. That is why `kid` on the URL is a thumbprint rather than an operator-chosen label: there is nothing a publisher could assert that the edge would have to trust. + +```mermaid +graph LR + subgraph "Exchange" + PRIV_KEY["Private Key
(signs URLs)"] + end + + subgraph "Edge Function (Provider CDN)" + PUB_KEY["Public Key
(verifies URLs)"] + end -Cost budget on a capable edge: HMAC-SHA256 (µs) + SHA-256 thumbprint (µs) + one Ed25519 verify (~50-100 µs) ≪ 1 ms, no socket. Because the RFC 9421 signature covers `@target-uri` + `created`, a captured fetch signature cannot be replayed against a different URL or outside the short TTL. + subgraph "Key Distribution" + WBA_DIR["Exchange key directory
or pinned config"] + end + + PRIV_KEY -->|"Signs: GET\n<canonical URL>"| SIGNED_URL[Signed URL] + SIGNED_URL -->|"Fetched"| PUB_KEY + PUB_KEY -->|"Verifies signature"| DECISION{Valid?} + PRIV_KEY -.->|"Public key published at"| WBA_DIR + WBA_DIR -.->|"Edge function fetches public key"| PUB_KEY +``` + +## The CloudFront-Native Path + +CloudFront verifies the signature at the infrastructure level, before any function code runs. + +```mermaid +sequenceDiagram + participant Agent as Fetcher + participant CDN as CloudFront + participant Edge as Edge Function + participant Origin as Origin Server + + Agent->>CDN: GET /premium/article.html?Signature=...&Key-Pair-Id=...&Expires=... + + Note over CDN: CloudFront verifies the RSA signature
against its trusted key group + + alt Signature invalid or expired + CDN-->>Agent: 403 Forbidden (CDN-generated) + else Signature valid + CDN->>Edge: Forward request (signature already verified) + Edge->>Origin: Forward to origin + Origin-->>Agent: 200 OK + content + end +``` + +Configure a trusted key group on the cache behaviour for the protected path and upload the RSA public key to CloudFront key management. The Exchange signs with a canned policy, producing `Expires`, `Signature` and `Key-Pair-Id`; CloudFront verifies them natively. + +`agent_id` is set on the resource URL before signing, so the CloudFront signature covers it and a URL holder cannot swap the value. But CloudFront cannot run code at verification time, so it cannot require proof of possession. This path is **bearer-only**: whoever holds the URL can fetch until it expires. Publishers needing the binding enforced run a code-capable edge. + +Akamai is not a supported target. EdgeAuth is a symmetric token scheme built on a secret shared with the CDN, which is the model RAMP deliberately does not use. ## Single-Use URL Enforcement -For providers wanting additional replay protection, the edge function can enforce single-use URLs via edge KV: +:::caution +Not implemented in the reference edge. This section describes the option and why it is not the primary defence. +::: + +A provider wanting additional replay protection can enforce single-use URLs through edge KV, keyed on the URL's `sig`. ```mermaid sequenceDiagram - participant Agent as AI Agent + participant Agent as Fetcher participant Edge as Edge Function participant KV as Edge KV Store - Agent->>Edge: GET /premium/article.html?sig=...&txn_id=txn-93a7 + Agent->>Edge: GET /premium/article.html?...&sig=k7Qm2xR9... Note over Edge: Signature verified OK - Edge->>KV: PUT-IF-ABSENT
key: "used:txn-93a7"
value: timestamp
TTL: 600s + Edge->>KV: PUT-IF-ABSENT
key: "used:<sig>"
value: timestamp
TTL: URL lifetime + 60s alt Key did not exist (first use) KV-->>Edge: success (inserted) @@ -207,60 +294,19 @@ sequenceDiagram end ``` -**KV TTL**: Set to `maxUrlTtlSeconds + 60s` (URL max TTL plus a safety buffer). After this period, the signed URL is expired anyway, so the KV entry can be evicted. - -**Single-use enforcement is best-effort.** CDN edge KV stores are eventually consistent across locations. The primary replay protection is agent identity binding + short TTL (5 minutes). Authoritative deduplication happens during reconciliation via CDN access logs and Exchange transaction records. - -**CDN KV consistency reality**: +**It would be best-effort.** Edge KV stores are eventually consistent across locations, so two near-simultaneous fetches in different regions can both see an absent key. | Platform | KV Mechanism | Consistency Model | Propagation Delay | |---|---|---|---| | CloudFront | No native KV; Lambda@Edge + DynamoDB | Per-region, eventually consistent (DynamoDB Global Tables) | Seconds across regions | | Cloudflare Workers KV | Workers KV | Eventually consistent | ~60s propagation across PoPs | | Cloudflare Durable Objects | Durable Objects | Strong consistency | Single-location (latency tradeoff) | -| Akamai EdgeKV | EdgeKV | Eventually consistent | ~5-10s in-region | | Fastly KV Store | KV Store | Eventually consistent | Seconds across PoPs | -**Recommendation: Do not enforce strict single-use at the edge.** Instead, use: - -1. **Agent identity binding** -- the signed URL is bound to a specific agent_id. Even if replayed, only the authorized agent can use it. -2. **Short TTL (5 minutes)** -- limits the replay window to a narrow period. -3. **Reconciliation (detect abuse after the fact)** -- the Exchange transaction log and CDN access logs contain `txn_id` for every request. Cross-reference to detect replays and take action. - -This matches ad-tech's approach to impression deduplication: real-time is best-effort, reconciliation is authoritative. - -## Key Custody Model +**The primary replay defences are the ones already in the request path:** -**The edge function verifies, it does not sign.** +1. **Agent identity binding** — the URL is bound to one key, and a replay by anyone else fails proof of possession. +2. **Short TTL** — five minutes in the reference deployment, which narrows the window. +3. **Reconciliation** — the Exchange transaction log and the edge delivery log both record the signed-URL hash. Cross-referencing them detects replay after the fact. -| Signing Scheme | Who Signs | Who Verifies | Edge Function Has | -|---|---|---|---| -| CloudFront RSA (asymmetric) | Exchange (private key) | CloudFront infra (public key) | Nothing (CDN-native) | -| Akamai EdgeAuth (symmetric) | Exchange (shared secret) | Akamai infra (shared secret) | Nothing (CDN-native) | -| HMAC-SHA256 (symmetric) | Exchange (shared secret) | Edge function (same secret) | **The shared secret** | -| RSA/Ed25519 (asymmetric, custom) | Exchange (private key) | Edge function (public key) | Public key only | - -**Concern with HMAC**: When using HMAC verification (Cloudflare, Fastly), the edge function must hold the shared secret. If the edge function is compromised, the attacker can forge signed URLs. Key rotation requires updating both the Exchange and the edge function simultaneously. - -**Recommendation**: Use asymmetric signing (Ed25519 or RSA) when the CDN does not provide native signed URL verification. - -```mermaid -graph LR - subgraph "Exchange" - PRIV_KEY["Private Key
(signs URLs)"] - end - - subgraph "Edge Function (Provider CDN)" - PUB_KEY["Public Key
(verifies URLs)"] - end - - subgraph "Key Distribution" - WBA_DIR["WBA directory or
config endpoint"] - end - - PRIV_KEY -->|"Signs: URL + expiry + agent_id + txn_id"| SIGNED_URL[Signed URL] - SIGNED_URL -->|"Agent fetches"| PUB_KEY - PUB_KEY -->|"Verifies signature"| DECISION{Valid?} - PRIV_KEY -.->|"Public key published at"| WBA_DIR - WBA_DIR -.->|"Edge function fetches public key"| PUB_KEY -``` +This matches ad-tech's approach to impression deduplication: real time is best-effort, reconciliation is authoritative. From 290b41f6b26c432c6c1e142ae92d745aa1ea9a8e Mon Sep 17 00:00:00 2001 From: noxlesh Date: Tue, 1 Sep 2026 17:33:59 +0300 Subject: [PATCH 03/16] docs(edge): correct the edge-function pages that assert a shared secret Three pages still told an operator to deploy a secret to the edge. The edge verifies with a public key and holds nothing secret, so each of these was not just a wrong algorithm name but a wrong instruction. overview.mdx framed the system as two signing subsystems needing "different cryptographic primitives", one symmetric and one asymmetric. Both are Ed25519. The real distinction is which key signs and who verifies, so the contrast is rewritten rather than relabelled. The request-flow pseudocode also claimed an expiry check against a max-TTL bound that no verifier implements, and described agent binding as conditional when it is on by default. cdn-adapters.mdx carried the false claim in executable shape: EdgeConfig declared signingMode and an optional hmacSecret. Those fields are replaced by the configuration the edge actually reads -- the Exchange key directory URL and optional pinned public keys. The Cloudflare note said the Worker "holds the signing secret or public key"; it holds only the public key, and that difference is the security property, not a detail. The Akamai EdgeWorkers section is removed and replaced by one paragraph saying Akamai is unsupported and why. EdgeAuth verifies with a secret shared with the CDN, which is exactly the model being retired here; documenting it as a supported target would reproduce the defect this change exists to fix. The platform matrix loses the Akamai column with it, and its verification row is retitled and re-derived: an Ed25519 verify is roughly 50-100 microseconds, so a CloudFront Function fails on the 1 ms budget and the absence of Ed25519 rather than on cost, Cloudflare passes natively, and Fastly passes with the injected verifier the SDK accepts for exactly that reason. deployment.mdx told an operator to run `wrangler secret put HMAC_SECRET`. That line is deleted rather than renamed -- there is no secret to put. The config table's signing-mode and quarterly-secret-rotation rows go with it, replaced by the directory URL and optional pinned keys. The secrets-management section now states the property plainly: nothing confidential is deployed to the edge, so the signing configuration may live in plain config or KV. composition.mdx needed no change. It already names the per-tenant selector correctly and is the model the other pages are brought in line with. --- .../components/edge-function/cdn-adapters.mdx | 80 ++++++------------- .../components/edge-function/deployment.mdx | 24 +++--- .../components/edge-function/overview.mdx | 16 ++-- 3 files changed, 46 insertions(+), 74 deletions(-) diff --git a/website/src/content/docs/components/edge-function/cdn-adapters.mdx b/website/src/content/docs/components/edge-function/cdn-adapters.mdx index 67fa099c..c5a173e9 100644 --- a/website/src/content/docs/components/edge-function/cdn-adapters.mdx +++ b/website/src/content/docs/components/edge-function/cdn-adapters.mdx @@ -76,11 +76,11 @@ interface EdgeConfig { /** Bot User-Agent patterns to detect (regexes). */ botPatterns: RegExp[]; - /** Signing verification mode: "cdn-native" | "hmac" */ - signingMode: "cdn-native" | "hmac"; + /** Exchange key directory the edge resolves `kid` against. */ + exchangeWbaUrl: string; - /** HMAC secret (only when signingMode is "hmac"). */ - hmacSecret?: string; + /** Optional pinned public keys; a covering `kid` skips the directory fetch. */ + verifyKeys?: JsonWebKey[]; /** Maximum signed URL TTL in seconds (enforced independently). */ maxUrlTtlSeconds: number; @@ -223,14 +223,14 @@ function handler(event) { | Package size | 10 MB | | Languages | JavaScript, TypeScript, Rust (WASM) | -**Key difference from CloudFront**: Cloudflare has no native signed URL verification. The Worker must implement HMAC or RSA verification in code. This means the Worker holds the signing secret or public key. +**Key difference from CloudFront**: Cloudflare has no native signed URL verification. The Worker verifies the Ed25519 signature in code, using **only** the Exchange's public key. Nothing secret is deployed to the edge. **Recommended approach**: Single Worker with optional Durable Objects. ```mermaid graph LR subgraph "Cloudflare Edge" - WORKER["Cloudflare Worker
Bot detection
ramp.json + rsl.txt serving
HMAC signature verification
Agent identity binding"] + WORKER["Cloudflare Worker
Bot detection
ramp.json + rsl.txt serving
Ed25519 signature verification
Agent identity binding"] KV["Workers KV
Config cache
Bot pattern updates"] DO["Durable Object
(optional)
Single-use enforcement
Atomic check-and-consume"] end @@ -239,47 +239,17 @@ graph LR WORKER --> DO ``` -- **Signature verification**: Must be done in Worker code. Use Web Crypto API for HMAC-SHA256. For RSA, use `crypto.subtle.verify` with imported public key. +- **Signature verification**: Must be done in Worker code. Use `crypto.subtle.verify` with the `Ed25519` algorithm and the imported public key. Workers support Ed25519 natively; Fastly does not, which is why the SDK's verifier accepts an injected implementation. - **Single-use enforcement**: Workers KV is eventually consistent (up to 60 seconds propagation). For strict single-use, use Durable Objects which provide transactional consistency. For soft single-use (acceptable eventual consistency), Workers KV with a short TTL is cheaper. - **Config**: Store `EdgeConfig` in Workers KV. Refresh on a timer or via Cron Trigger. -## Akamai EdgeWorkers +## Akamai -**Runtime**: Akamai EdgeWorkers (JavaScript, V8-based). - -**Capabilities and limitations**: - -| Capability | EdgeWorker | -|---|---| -| Execution time limit | 4ms (onClientRequest), 4ms (onClientResponse) | -| Memory | 2 MB per event handler | -| Network access | Yes (httpRequest -- limited, async) | -| KV access | EdgeKV (eventually consistent, ~5-10s in-region) | -| Native signed URL verification | Akamai EdgeAuth tokens (property-level config) | -| Package size | 1 MB | -| Languages | JavaScript (ES6+), limited stdlib | - -**Key considerations**: -- Akamai's EdgeAuth token system can verify signed URLs at the property level (similar to CloudFront `trusted_key_groups`). Configure EdgeAuth on the property manager for protected paths. -- EdgeKV provides edge-local key-value storage for single-use enforcement, but with eventual consistency (~5-10 seconds within a region). -- The 4ms execution limit is tight. Bot detection (string matching) and ramp.json serving fit. HMAC verification with Web Crypto fits. HTTP subrequests do not fit in 4ms. -- Complex logic (agent identity binding with HTTP subrequests) should be moved to the `onOriginRequest` handler (longer timeout, up to 120s). - -**Recommended approach**: - -```mermaid -graph LR - subgraph "Akamai Edge" - EW_CLIENT["EdgeWorker: onClientRequest
Bot detection
ramp.json + rsl.txt serving
EdgeAuth passthrough"] - EW_ORIGIN["EdgeWorker: onOriginRequest
Agent identity binding
Single-use enforcement"] - EKV["EdgeKV
Config + single-use tracking"] - EDGEAUTH["Property Manager
EdgeAuth token validation
on /premium/* paths"] - end - - EW_CLIENT --> EDGEAUTH - EDGEAUTH --> EW_ORIGIN - EW_ORIGIN --> EKV -``` +Akamai is not a supported target. Its EdgeAuth token scheme verifies with a secret shared +with the CDN, which is the model RAMP deliberately does not use: every RAMP delivery +endpoint verifies with a public key and holds nothing secret. Running RAMP on Akamai would +mean writing the Ed25519 verifier as an EdgeWorker, which the platform's 4 ms budget and +2 MB handler memory make an open question rather than a supported path. ## Fastly Compute@Edge @@ -307,7 +277,7 @@ graph LR ```mermaid graph LR subgraph "Fastly Edge" - COMPUTE["Compute Service (WASM)
Bot detection
ramp.json + rsl.txt serving
HMAC/RSA verification
Agent identity binding
Single-use enforcement"] + COMPUTE["Compute Service (WASM)
Bot detection
ramp.json + rsl.txt serving
Ed25519 signature verification
Agent identity binding
Single-use enforcement"] KV_STORE["KV Store
Config + single-use tracking
(strongly consistent in-POP)"] CONFIG_STORE["Config Store
Signing keys, bot patterns"] end @@ -318,14 +288,14 @@ graph LR ## Platform Comparison Matrix -| Feature | CloudFront Function | CloudFront + Lambda@Edge | Cloudflare Worker | Akamai EdgeWorker | Fastly Compute | -|---|---|---|---|---|---| -| Bot detection | Yes | Yes | Yes | Yes | Yes | -| ramp.json + rsl.txt serving | Yes | Yes | Yes | Yes | Yes | -| Native signed URL verification | Yes (trusted_key_groups) | Yes (trusted_key_groups) | No (must implement) | Yes (EdgeAuth) | No (must implement) | -| Custom HMAC verification | No (1ms limit) | Yes | Yes | Tight (4ms limit) | Yes | -| Agent identity binding | No (no network) | Yes (DynamoDB lookup) | Yes | Yes (onOriginRequest) | Yes | -| Single-use enforcement | No (no KV write) | Yes (DynamoDB) | Yes (Durable Objects) | Yes (EdgeKV, eventual) | Yes (KV Store, strong) | -| Consistency model | N/A | Strong (DynamoDB) | Strong (Durable Objects) | Eventual (~5-10s) | Strong (in-POP) | -| Latency overhead | <0.1ms | 1-5ms | <1ms | <1ms | <1ms | -| Cost at 1M req/day | $0.10 | $0.70 + DDB | $0.50 | Tier-based | Usage-based | +| Feature | CloudFront Function | CloudFront + Lambda@Edge | Cloudflare Worker | Fastly Compute | +|---|---|---|---|---| +| Bot detection | Yes | Yes | Yes | Yes | +| ramp.json + rsl.txt serving | Yes | Yes | Yes | Yes | +| Native signed URL verification | Yes (trusted_key_groups) | Yes (trusted_key_groups) | No (must implement) | No (must implement) | +| Ed25519 signed-URL verification | No (1ms limit, no Ed25519) | Yes | Yes (native Web Crypto) | Yes (injected verifier) | +| Agent identity binding | No (no network) | Yes | Yes | Yes | +| Single-use enforcement | No (no KV write) | Yes (DynamoDB) | Yes (Durable Objects) | Yes (KV Store, strong) | +| Consistency model | N/A | Strong (DynamoDB) | Strong (Durable Objects) | Strong (in-POP) | +| Latency overhead | <0.1ms | 1-5ms | <1ms | <1ms | +| Cost at 1M req/day | $0.10 | $0.70 + DDB | $0.50 | Usage-based | diff --git a/website/src/content/docs/components/edge-function/deployment.mdx b/website/src/content/docs/components/edge-function/deployment.mdx index be64c4ce..c0eb9b80 100644 --- a/website/src/content/docs/components/edge-function/deployment.mdx +++ b/website/src/content/docs/components/edge-function/deployment.mdx @@ -105,8 +105,8 @@ When available, the CLI will generate the platform-specific deployment artifact | Contact email | Provider | Rarely | `licensing@techcrunch.com` | | Protected path patterns | Provider editorial policy | Monthly | `["/premium/*", "/archive/*"]` | | Bot User-Agent patterns | RAMP community list | Weekly | `["ClaudeBot", "GPTBot", ...]` | -| Signing mode | Deployment architecture | Never | `"cdn-native"` or `"hmac"` | -| HMAC secret | Security team | Quarterly (rotation) | `"hmac-secret-..."` | +| Exchange key directory | Provider-Exchange contract | Rarely | `https://exchange.example/.well-known/http-message-signatures-directory` | +| Pinned verify keys | Optional; skips the directory fetch | Rarely | `[{"kty":"OKP","crv":"Ed25519",...}]` | | Max URL TTL | Policy | Rarely | `300` (seconds) | | Single-use enabled | Policy | Rarely | `true` | | Agent binding enabled | Policy | Rarely | `false` | @@ -197,12 +197,12 @@ interface EdgeConfigDocument { /** Signed URL verification configuration. */ signing: { - /** Verification mode. */ - mode: "cdn-native" | "hmac"; - /** Maximum URL TTL in seconds (enforced independently of CDN). */ - max_ttl_seconds: number; - /** Whether to enforce agent identity binding. */ - agent_binding_enabled: boolean; + /** Exchange key directory the edge resolves `kid` against. */ + exchange_wba_url: string; + /** Optional pinned public keys; a covering `kid` skips the fetch. */ + verify_keys?: JsonWebKey[]; + /** Whether to enforce agent identity binding. Defaults to true. */ + enforce_binding: boolean; /** Whether to enforce single-use URLs (best-effort, see Section 5.3). */ single_use_enabled: boolean; }; @@ -211,8 +211,8 @@ interface EdgeConfigDocument { ## Secrets Management -- HMAC secrets and signing keys are **environment variables** or **CDN secret stores** (Cloudflare Secrets, AWS Secrets Manager, Akamai Property Manager variables). Never stored in KV. -- Secrets are loaded once at function initialization (cold start), not per-request. +- **The edge function holds no secret.** It verifies with a public key, so its signing configuration is a directory URL and, optionally, pinned public keys. Neither is confidential, and both may live in plain configuration or KV. +- Configuration is loaded once at function initialization (cold start), not per-request. Where a deployment does carry a secret for unrelated reasons, it belongs in the platform secret store, never in KV. ### Platform-Specific Wrangler Deployment (Cloudflare) @@ -225,8 +225,8 @@ cd packages/edge-function && npx wrangler dev # Production deploy cd packages/edge-function && npx wrangler deploy -# Set secrets (never in wrangler.toml) -npx wrangler secret put HMAC_SECRET +# Point the worker at the Exchange key directory +npx wrangler deploy --var EXCHANGE_WBA_URL=https://exchange.example/.well-known/http-message-signatures-directory ``` ### CDN-Specific Deployment Commands diff --git a/website/src/content/docs/components/edge-function/overview.mdx b/website/src/content/docs/components/edge-function/overview.mdx index eea0411c..78671670 100644 --- a/website/src/content/docs/components/edge-function/overview.mdx +++ b/website/src/content/docs/components/edge-function/overview.mdx @@ -33,10 +33,12 @@ Target: **under 10 minutes** from Exchange operator contract to live edge functi ## Signing Scope -The edge function deals exclusively with **signed URL verification** (HMAC-SHA256 or CDN-native RSA). It does NOT verify offer signatures. Offer signatures use Ed25519 and are verified by agents and Brokers, not the edge function. The two signing subsystems are independent: +The edge function deals exclusively with **signed URL verification**. It does NOT verify offer signatures. Both subsystems use Ed25519; they differ in which key signs and who verifies: -- **Signed URLs** (Exchange to CDN): HMAC-SHA256 -- shared secret between Exchange and edge function -- **Offer signatures** (Exchange to Agent/Broker): Ed25519 -- asymmetric, verified via public key +- **Signed URLs** (Exchange to delivery endpoint): the Exchange signs with the tenant's key; the edge verifies with the published public key, or CloudFront verifies an RSA signature natively. +- **Offer signatures** (Exchange to Agent/Broker): the Exchange signs with its offer key; agents and Brokers verify with the published public key. + +One primitive, two audiences. Neither carries a shared secret. ## How It Fits in the RAMP Architecture @@ -95,10 +97,10 @@ handleRequest(ctx): -> Pass through (no edge function involvement) 3. If request has signed URL parameters: - a. Verify signature (CDN-native or HMAC) + a. Verify the Ed25519 signature (or CDN-native RSA) b. If signature invalid -> 403 (always, regardless of bot status) - c. Check expiry (reject if > maxUrlTtlSeconds from issuance) - d. If agentBindingEnabled: validate agent_id param + c. Check expiry (reject if exp has passed) + d. Agent binding is enforced by default: validate agent_id param e. If singleUseEnabled: atomic check-and-consume via KV f. All pass -> allow request through to origin/cache g. Any fail -> 403 with specific error @@ -131,7 +133,7 @@ When a request arrives with signed URL parameters, the edge function verifies th The edge function must add **< 5ms** to request latency (p99). Per-request operations are strictly limited to: - String comparison (User-Agent matching against bot patterns) -- Signature verification (HMAC-SHA256 or RSA via Web Crypto API) +- Signature verification (Ed25519 via Web Crypto, or CDN-native RSA) - KV read (agent_id check, if agent binding is enabled) - URL parameter parsing From ba9b779ac9707f78d03c57c902f3da77ef026d3a Mon Sep 17 00:00:00 2001 From: noxlesh Date: Tue, 1 Sep 2026 17:35:05 +0300 Subject: [PATCH 04/16] docs(exchange): correct the Exchange signing-engine and storage pages These three pages carried the strongest statement of the false model anywhere on the site: not just that signed URLs use HMAC, but a stated rationale for why they should. overview.mdx argued that "different trust models require different cryptographic primitives", and storage-model.mdx said symmetric signing was appropriate "because the Exchange and the CDN edge function share a secret". They never did. Both signing subsystems are Ed25519, and what actually separates them is which key signs and who verifies -- offers are verified by agents and Brokers, delivery URLs by the delivery endpoint. The rationale is rewritten, not relabelled, because a corrected algorithm name under a false argument would have left the page still teaching the wrong idea. The Go samples described a struct that does not exist. CDNType with a CDNGenericHMAC member was a third spelling of a concept the tenant table already names signing_scheme, and PrivateKey was documented as "PEM-encoded private key or HMAC secret". Both samples now mirror the shape the Exchange really dispatches on, with the two values the enum really has, so a reader can check them against the code. The canonicalization section described a four-field string joined by newlines. The signed message is the literal GET, one newline, then the whole canonical URL with sig removed and the query sorted -- which covers scheme, host, path and every publisher query parameter, not four selected fields. Rather than restate the rules in a second place where they can drift again, the section now states the shape briefly and points at the normative page, and adds the fact this page is the right home for: reconciliation joins on signed_url_hash, recorded by both the Exchange and the delivery endpoint, because the txn_id parameter it referred to does not exist. --- .../docs/components/exchange/multi-tenant.mdx | 6 ++-- .../docs/components/exchange/overview.mdx | 8 ++--- .../components/exchange/storage-model.mdx | 34 ++++++------------- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/website/src/content/docs/components/exchange/multi-tenant.mdx b/website/src/content/docs/components/exchange/multi-tenant.mdx index 1bd06a6a..9b7dd771 100644 --- a/website/src/content/docs/components/exchange/multi-tenant.mdx +++ b/website/src/content/docs/components/exchange/multi-tenant.mdx @@ -30,9 +30,9 @@ type TenantConfig struct { PricingOverrides map[string]*rampv1.Pricing // path pattern -> pricing // Content delivery - ContentBaseURL string // CDN base URL (e.g., "https://cdn.techcrunch.com") - CDNType CDNType // CloudFront, Akamai, Fastly, GenericHMAC - SigningKeyRef string // Reference to key in secrets manager + ContentBaseURL string // CDN base URL (e.g., "https://cdn.techcrunch.com") + Scheme SigningScheme // ED25519 or AWS_CLOUDFRONT_RSA + SigningKeyRef string // Reference to key in secrets manager // Signed URL configuration URLTTLSeconds int // Default: 300 (5 minutes) diff --git a/website/src/content/docs/components/exchange/overview.mdx b/website/src/content/docs/components/exchange/overview.mdx index 903ed30b..d93e6a60 100644 --- a/website/src/content/docs/components/exchange/overview.mdx +++ b/website/src/content/docs/components/exchange/overview.mdx @@ -18,7 +18,7 @@ The Exchange implements the RAMP v1.0 `ExchangeService` (4 RPCs: `DiscoverResour | **Catalog Management** | Maintains an in-memory resource catalog (radix trie) per provider tenant, built from RSL, sitemaps, CMS APIs, and third-party intelligence providers. Scope-based catalog filtering ensures requesters only see resources matching their declared scopes | | **Pricing** | Computes offer pricing from Exchange config overrides, RSL-derived pricing, and tenant defaults. Includes unit_cost normalization for cross-provider comparison | | **Transaction Execution** | Stateless offer verification (Ed25519), billing authorization via pluggable adapter, durable transaction logging (write-before-sign invariant). Subscription offers include `SubscriptionQuotaInfo` for proactive quota signaling | -| **Signed URL Generation** | Per-tenant HMAC-SHA256 signed URLs for CDN-verified resource delivery, with configurable TTL and requester identity binding | +| **Signed URL Generation** | Per-tenant Ed25519 signed URLs, or CloudFront RSA where the CDN verifies natively, with configurable TTL and requester identity binding | | **Usage Reporting** | Tracks reporting obligations per transaction, enforces compliance rates, blocks non-compliant agents. Returns `report_id` for dispute chain linkage (v1.0) | | **Attestation Verification (v1.0)** | Verifies `ResourceAttestation` signatures at catalog push time using keys from the verifier's WBA directory (the JWK Set at `/.well-known/http-message-signatures-directory`). Propagates valid attestations to offers | | **Dispute Resolution (v1.0)** | Accepts `DisputeTransaction` RPCs, auto-resolves disputes using CDN logs and cryptographic evidence (Tier 1), applies rule-based review (Tier 2), and escalates patterns (Tier 3) | @@ -78,7 +78,7 @@ The Exchange Node has five internal modules. Each module owns a distinct respons 2. **Transaction Engine** verifies the offer's signature (Ed25519 for offers, stateless — no catalog lookup), then calls the Billing Adapter, then writes to the Transaction Log, then calls the Signing Engine. This is a sequential pipeline — no parallelism within a single transaction. Offers are stateless: no OfferStore, no OfferCache, no ResolveOffer. 3. **Reporting Tracker** is called by the ReportUsage RPC handler. It reads the obligation from the Transaction Log to validate the report, then updates the obligation state. 4. **RSL Ingestion** runs on a configurable interval (default: 5 minutes). It fetches, parses, diffs, and produces a serialized catalog binary that the Exchange loads via atomic pointer swap. Source priority is defined in the Resource Ingestion Pipeline design doc. -5. **Signing Engine** has two subsystems: (a) Ed25519 for offer signatures (asymmetric — agents and Brokers verify with the public key), and (b) HMAC-SHA256 for signed URLs (symmetric — shared secret between Exchange and CDN). Both are pure function calls — no external I/O. Keys are loaded at startup and rotated via a background goroutine watching a key store. +5. **Signing Engine** has two subsystems, both asymmetric: (a) Ed25519 for offer signatures, verified by agents and Brokers with the published public key, and (b) delivery-URL signatures, verified by the delivery endpoint — Ed25519 at a code-capable edge, or RSA where CloudFront verifies natively. They differ in which key signs and who verifies, not in the primitive. Both are pure function calls — no external I/O. Keys are loaded at startup and rotated via a background goroutine watching a key store. ## Module Dependency Graph @@ -124,14 +124,14 @@ The Exchange sits between AI agents/Brokers and provider CDN infrastructure: 1. An AI agent discovers available resources by calling `DiscoverResources` — the Exchange verifies the `Requester` identity (Ed25519 signature over domain-bound key), filters the catalog by the requester's scopes, and returns signed offers. If a `Delegation` is present, the Exchange verifies the delegation JWT chain — including the **holder-binding** step (the request-signing key MUST hash to the final JWT's `cnf.jkt`; see [Verification](/protocol/authentication/#verification)) — before granting scoped access. When the request was forwarded through one or more intermediaries, each adds a labeled RFC 9421 HTTP Message Signature to the header stack (each covering the request plus the prior hop's signature); the Exchange verifies the whole stack to prove the request path and counts hops against `max_hops` / `max_intermediary_hops` 2. The agent executes a transaction by calling `ExecuteTransaction` with a chosen offer — the Exchange verifies the offer signature, authorizes billing, logs the transaction, and returns a signed URL -3. The agent fetches the resource using the signed URL — the CDN edge function verifies the URL signature using the same HMAC secret shared with the Exchange +3. The agent fetches the resource using the signed URL — the delivery endpoint verifies the URL signature with the Exchange's published public key, holding nothing secret 4. The agent reports usage by calling `ReportUsage` — the Exchange validates the report and updates the obligation state ## Key Design Decisions - **Stateless offer verification**: Offers are Ed25519-signed tokens. ExecuteTransaction verifies and decodes the offer from its signature — no offer storage, no offer cache, no re-resolution against the catalog. This eliminates a class of consistency bugs and simplifies horizontal scaling. - **Write-before-sign invariant**: The transaction log write MUST complete before the Signing Engine generates the signed URL. No signed URL is ever issued for an unrecorded transaction. -- **Two signing subsystems**: Ed25519 for offer signatures (asymmetric, agents verify with public key) and HMAC-SHA256 for signed URLs (symmetric, shared secret between Exchange and CDN). Different trust models require different cryptographic primitives. +- **Two signing subsystems, one primitive**: Ed25519 for offer signatures, verified by agents and Brokers, and Ed25519 for delivery URLs, verified by the delivery endpoint (RSA where CloudFront verifies natively). Both are asymmetric and neither shares a secret; separating them isolates the keys and their audiences, not the algorithms. - **Pluggable Billing Adapter**: The single point of custom integration per deployment. The open-source Exchange is billing-system-agnostic. - **Copy-on-write catalog**: The resource catalog uses `atomic.Pointer` for lock-free reads on the hot path. The ingestion pipeline builds a new catalog snapshot in the background and swaps it atomically. - **Single currency per deployment (v1)**: No cross-currency conversion. The Exchange operator sets a single operating currency. Cross-currency is a v2 feature. diff --git a/website/src/content/docs/components/exchange/storage-model.mdx b/website/src/content/docs/components/exchange/storage-model.mdx index 0beb5f48..69e633c7 100644 --- a/website/src/content/docs/components/exchange/storage-model.mdx +++ b/website/src/content/docs/components/exchange/storage-model.mdx @@ -304,24 +304,22 @@ type TenantSigningConfig struct { // CDN should accept signatures from both keys during rotation. PreviousKey *SigningKey - // CDN type determines the signing algorithm. - CDNType CDNType // CloudFront, Akamai, Fastly, GenericHMAC + // Which delivery-URL scheme this tenant uses. + Scheme SigningScheme } type SigningKey struct { - KeyID string // Key-Pair-Id (CloudFront), token name (Akamai), etc. - PrivateKey []byte // PEM-encoded private key or HMAC secret + KeyID string // Key-Pair-Id for CloudFront; the key thumbprint for Ed25519 + PrivateKey []byte // Ed25519 private key, or a PEM-encoded RSA private key ValidFrom time.Time ValidUntil time.Time // 90-day max per NFR } -type CDNType int +type SigningScheme string const ( - CDNCloudFront CDNType = iota - CDNAkamai - CDNFastly - CDNGenericHMAC + SchemeEd25519 SigningScheme = "ED25519" + SchemeCFRSA SigningScheme = "AWS_CLOUDFRONT_RSA" ) ``` @@ -367,20 +365,10 @@ The `report_id` linkage is critical -- it connects the dispute to the agent's pr **Storage**: Dispute records share the same durable store as transaction records (PostgreSQL for Growth tier, ClickHouse for Scale tier). They are indexed by `dispute_id`, `transaction_id`, and `(billing_ref, filed_at)` for compliance queries. -## Signed URL HMAC Canonicalization +## Signed URL Canonicalization -This section applies to **signed URLs only** (content delivery via CDN), not to offer signatures. Signed URLs use HMAC-SHA256 because the Exchange and the CDN edge function share a secret — this is a two-party relationship where symmetric signing is appropriate. Offer signatures use Ed25519 (see the Signing Keys section and the OfferSigner interface). +This section applies to **signed URLs only** (content delivery), not to offer signatures. Both use Ed25519; they differ in which key signs and who verifies. -Both the Exchange (signer) and the edge function (verifier) MUST use the same canonical format for HMAC input. Signed URL fields are concatenated with `\n` delimiters in this fixed order: +The signed message is the literal `GET`, one newline, then the canonical URL: the whole URL with the `sig` parameter removed and the remaining query sorted by key. Scheme, host and path are covered verbatim, and so is every publisher query parameter. The signature is base64url with no padding. The full rules, including the parameter set and the verification order, are on the [signed URL verification](/components/edge-function/signed-url-verification) page, which is normative. -``` -baseURL\nexpires\nagent_id\ntxn_id -``` - -Where: -- `baseURL` — the content URL without query parameters (e.g., `https://cdn.example.com/premium/article.html`) -- `expires` — Unix timestamp in seconds (string representation) -- `agent_id` — the agent's RFC 7638 JWK Thumbprint (matches `TransactionResponse.agent_identity_hash`) -- `txn_id` — the transaction ID (ULID) - -This format is shared between the Exchange SignURL implementation and the edge function verification logic. See the edge function design doc for the verifier implementation. +The Exchange records `signed_url_hash` — SHA-256 of the URL verbatim — on the transaction, and the delivery endpoint records the same hash on its delivery event. That pair is the join key for three-sided reconciliation; there is no transaction-id URL parameter. From 60397f77fa07bbddb2e041ae591e8be2d638ff57 Mon Sep 17 00:00:00 2001 From: noxlesh Date: Tue, 1 Sep 2026 17:38:52 +0300 Subject: [PATCH 05/16] docs(protocol): correct the protocol pages and walkthroughs Twelve pages carried the same false claim in three shapes: prose assertions, sample URL literals, and two security countermeasures that described defences nobody built. The URL literals were the widest problem. Every walkthrough showed ?expires=...&agent_id=...&txn_id=...&sig=hmac-sha256-..., and all four parts were wrong: the expiry parameter is exp, there is no txn_id parameter at all, the signature is Ed25519 rather than an HMAC digest, and there is a kid parameter the examples never showed. They now show the real parameter set in the order the canonical encoding produces, which is lexicographic. The EU-regulation example is the useful one to read: its publisher parameter uri sorts last and is covered by the signature like everything else, which the old four-field canonicalization could not express. Wherever a page said txn_id enables three-sided reconciliation, the replacement is not a rename. Reconciliation joins on the signed-URL hash -- SHA-256 of the URL verbatim -- which the Exchange records on the transaction and the delivery endpoint records on its delivery event. Neither side chooses the value, which is the property that makes the join trustworthy. Two threat-model countermeasures needed more than a substitution. T7 said the provider holds the URL-signing private key and the Exchange calls out to a provider signing service; no such service exists, and the Exchange signs with a per-tenant key it holds itself. The countermeasure now states what actually defends against the threat: per-tenant key isolation, a public verification key, and no signing capability distributed anywhere. T5 promised a transaction_id URL parameter for reconciliation and is corrected to the hash join. Neither line contained the string HMAC, so a grep for the defect would have missed both. scenario-walkthrough.mdx had a third defect the sweep exposed. Its fetch trace verified agent binding as SHA256(requester.id + ":" + requester.domain), which binds nothing -- an identifier is not a secret and an attacker can recompute it. The check is a thumbprint comparison against the presented public key, followed by an RFC 9421 proof that the fetcher holds the private half. The page's own caution already said hashing an identifier is not a binding; the trace contradicted it. publisher-onboarding.mdx told an operator to generate HMAC secrets for Akamai and Fastly, and its key-type table classified three of four rows as symmetric. Fastly runs the Ed25519 verifier, so that row was not merely outdated wording. The table is now two rows, both asymmetric, with the cdn_type value each one advertises -- and it says what actually differs between them, which is whether the delivery endpoint can run code and therefore enforce agent binding. --- .../architecture/production-architecture.mdx | 2 +- .../docs/getting-started/poc-walkthrough.mdx | 22 ++++--- .../getting-started/publisher-onboarding.mdx | 23 +++---- .../content/docs/protocol/authentication.mdx | 6 +- .../docs/protocol/scenario-walkthrough.mdx | 66 ++++++++++--------- .../docs/protocol/transaction-flow.mdx | 4 +- .../docs/protocol/walkthrough-academic.mdx | 12 ++-- .../protocol/walkthrough-credit-report.mdx | 10 +-- .../protocol/walkthrough-due-diligence.mdx | 8 +-- .../protocol/walkthrough-eu-regulation.mdx | 11 ++-- .../protocol/walkthrough-medical-imaging.mdx | 8 +-- .../content/docs/protocol/walkthrough-v1.mdx | 14 ++-- .../content/docs/security/threat-model.mdx | 6 +- 13 files changed, 96 insertions(+), 96 deletions(-) diff --git a/website/src/content/docs/architecture/production-architecture.mdx b/website/src/content/docs/architecture/production-architecture.mdx index 681b4678..76d8478c 100644 --- a/website/src/content/docs/architecture/production-architecture.mdx +++ b/website/src/content/docs/architecture/production-architecture.mdx @@ -126,7 +126,7 @@ The `TransactionResponse` carries a `DeliveryMethod` hint and a `retrieval_endpo - The Exchange may populate `retrieval_endpoint` with a signed URL, an access token, or any other retrieval mechanism - The agent fetches content from the provider's CDN using whatever credentials the Exchange provided -- The CDN verifies access independently (e.g., HMAC signature check, token validation) +- The CDN verifies access independently (e.g., an Ed25519 or RSA signature check, token validation) The protocol does not specify signed URL formats, CDN verification logic, or content transfer mechanisms. These are defined by each Exchange and Edge Function implementation. diff --git a/website/src/content/docs/getting-started/poc-walkthrough.mdx b/website/src/content/docs/getting-started/poc-walkthrough.mdx index 616526db..d022bd91 100644 --- a/website/src/content/docs/getting-started/poc-walkthrough.mdx +++ b/website/src/content/docs/getting-started/poc-walkthrough.mdx @@ -187,7 +187,7 @@ Response: { "transaction_id": "txn-1773682700352684307", "billing_id": "bill-000002", - "retrieval_endpoint": "https://cdn.ramp-protocol.org/premium/article.html?agent_id=NzbLsXh8...C9Xs&expires=1773683000&sig=3ab495e3...48823a14&txn_id=txn-1773682700352684307", + "retrieval_endpoint": "https://cdn.ramp-protocol.org/premium/article.html?agent_id=NzbLsXh8...C9Xs&exp=1773683000&kid=vJ3xR1mQ...&sig=3ab495e3...48823a14", "cost": { "amount": "0.05", "currency": "USD" }, "delivery_method": "DELIVERY_METHOD_INSTRUCTIONS", "reporting_obligation": { @@ -204,18 +204,20 @@ Response: Key fields: - **transaction_id** — unique transaction reference, recorded in the [transaction log](/components/exchange/storage-model) before the signed URL is generated - **billing_id** — billing record reference, managed by the [Billing Adapter](/components/exchange/billing-adapter) -- **retrieval_endpoint** — the [signed URL](/components/edge-function/signed-url-verification) with HMAC-SHA256 signature, expiry, agent identity binding, and transaction ID +- **retrieval_endpoint** — the [signed URL](/components/edge-function/signed-url-verification) with an Ed25519 signature, expiry, and agent identity binding - **cost** — $0.05 USD charged. See [how money flows](/getting-started/how-money-flows) for the full billing lifecycle - **reporting_obligation** — you MUST report usage within 24 hours. See [budget and reporting](/components/agent-sdk/budget-reporting) for how the Agent SDK handles this - **agent_identity_hash** — RFC 7638 JWK Thumbprint of your Ed25519 request-signing key, bound into the signed URL to [prevent URL sharing](/security/threat-model). See [Signed URL Verification](/components/edge-function/signed-url-verification) ### Step 5: Fetch Resource via [Signed URL](/components/edge-function/signed-url-verification) -The signed URL contains four verification parameters, all verified by the [CDN edge function](/components/edge-function/overview): -- `sig` — HMAC-SHA256 signature (Exchange↔CDN shared secret). See [signed URL verification](/components/edge-function/signed-url-verification) for the canonical format -- `expires` — Unix timestamp (URL expires after 5 minutes) -- `agent_id` — the agent's RFC 7638 JWK Thumbprint (identity binding — [prevents URL sharing](/security/threat-model)) -- `txn_id` — transaction ID (enables three-sided reconciliation: CDN logs + Exchange logs + agent reports) +The signed URL carries these parameters, verified by the [delivery endpoint](/components/edge-function/overview): +- `sig` — the Ed25519 signature, base64url with no padding. See [signed URL verification](/components/edge-function/signed-url-verification) for the canonical message +- `exp` — Unix timestamp (the URL expires after 5 minutes) +- `kid` — thumbprint of the Exchange key that signed it, which the edge resolves to a public key +- `agent_id` — the agent's RFC 7638 JWK Thumbprint (identity binding — [prevents URL sharing](/security/threat-model)); optional, and absent on a bearer URL + +Three-sided reconciliation joins on the signed-URL hash, recorded by both the Exchange and the delivery endpoint — not on a parameter in the URL. In production, the [edge function](/components/edge-function/overview) also performs [bot detection](/components/edge-function/bot-detection) — blocking unauthorized AI crawlers with a 403 and `X-Content-Rules` header that [redirects to the Exchange](/protocol/discovery-paths). @@ -338,7 +340,7 @@ The tool negotiates with the Exchange and returns: [RAMP v1.0 | Cost: 0.0500 USD | Transaction: txn-1773682640975288995 | Billing: bill-000001 | Usage reported: True] -[Content delivered via signed URL with HMAC-SHA256 + agent identity binding] +[Content delivered via Ed25519-signed URL + agent key proof-of-possession] --- RAMP Cost Summary --- Total data acquisition cost: $0.0500 @@ -420,9 +422,9 @@ For production deployment, see: **Security at every boundary** ([full threat model](/security/threat-model)): - Agent signs requests with [**Ed25519**](/protocol/authentication) (proves identity) - Exchange signs offers with [**Ed25519**](/protocol/authentication) (stateless verification) -- Signed URLs use [**HMAC-SHA256**](/components/edge-function/signed-url-verification) (Exchange↔CDN shared secret) +- Signed URLs use [**Ed25519**](/components/edge-function/signed-url-verification) (the delivery endpoint verifies with a public key and holds nothing secret) - Agent identity bound into signed URL (prevents [URL sharing](/security/threat-model)) -- Transaction ID in signed URL (enables [three-sided reconciliation](/components/exchange/storage-model)) +- Signed-URL hash recorded on both sides (enables [three-sided reconciliation](/components/exchange/storage-model)) ## Next Steps diff --git a/website/src/content/docs/getting-started/publisher-onboarding.mdx b/website/src/content/docs/getting-started/publisher-onboarding.mdx index b25cd5fd..8593a473 100644 --- a/website/src/content/docs/getting-started/publisher-onboarding.mdx +++ b/website/src/content/docs/getting-started/publisher-onboarding.mdx @@ -69,14 +69,11 @@ ramp-cli Exchange Provider CDN ### Generate a signing key ```bash -# CloudFront (ECDSA P-256 recommended) -ramp-cli key generate --cdn cloudfront --algorithm ecdsa-p256 +# A code-capable edge (Cloudflare, Fastly, Lambda@Edge) verifies Ed25519 +ramp-cli key generate --cdn edge-ed25519 -# Akamai (HMAC-SHA256) -ramp-cli key generate --cdn akamai - -# Fastly (HMAC-SHA256) -ramp-cli key generate --cdn fastly +# AWS CloudFront verifies RSA natively, via a trusted key group +ramp-cli key generate --cdn cloudfront --algorithm rsa-2048 ``` ### Push key to an Exchange @@ -207,14 +204,12 @@ See [Content Attestation](/protocol/content-attestation/) for the full attestati ## CDN Key Types -| CDN | Key Type | Algorithm | -|---|---|---| -| CloudFront | Asymmetric | ECDSA P-256 (recommended) or RSA-2048 | -| Akamai | Symmetric | Auth Token 2.0 (HMAC-SHA256) | -| Fastly | Symmetric | Custom VCL (HMAC-SHA256) | -| Generic | Symmetric | HMAC-SHA256 | +| Delivery endpoint | `cdn_type` | Key Type | Algorithm | +|---|---|---|---| +| Edge function (Cloudflare, Fastly, Lambda@Edge) | `edge-ed25519` | Asymmetric | Ed25519 | +| AWS CloudFront | `cloudfront` | Asymmetric | RSA-2048 | -CloudFront's asymmetric model provides the strongest security separation: the Exchange holds the private key for signing, CloudFront holds the public key for verification. +Both are asymmetric, so the separation is universal: the Exchange holds the private key for signing and the delivery endpoint holds only the public key for verification. What differs is whether the delivery endpoint can run code — and therefore whether it can enforce agent identity binding. CloudFront verifies natively but cannot, so its URLs are bearer credentials; an edge function does both. ## Onboarding Checklist diff --git a/website/src/content/docs/protocol/authentication.mdx b/website/src/content/docs/protocol/authentication.mdx index 4c51e683..32cfc16d 100644 --- a/website/src/content/docs/protocol/authentication.mdx +++ b/website/src/content/docs/protocol/authentication.mdx @@ -379,10 +379,10 @@ RFC 9421 is the **only** request-authentication mechanism. A request without a v A signed `retrieval_endpoint` returned by a transaction is, by default, a bearer token: anyone holding it before `expires_at` can fetch. RAMP **optionally** binds the URL to the purchasing agent, DPoP-style ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)): -- The Exchange embeds `agent_identity_hash` — the [RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) JWK Thumbprint (SHA-256) of the agent's Ed25519 request-signing key — inside the HMAC-signed URL, and echoes it on the execute-path response (`TransactionResponse.agent_identity_hash`, produced directly by the Exchange — the Broker is not in the execute response path for this field). -- A capable delivery endpoint (edge function) verifies the binding **fully offline**: confirm the URL HMAC (proves the hash is Exchange-issued and untampered), then require the fetcher to present its public key and an RFC 9421 signature over the retrieval request, and check `thumbprint(presented key) == agent_identity_hash`. No JWKS fetch required. +- The Exchange embeds `agent_identity_hash` — the [RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) JWK Thumbprint (SHA-256) of the agent's Ed25519 request-signing key — as the URL's `agent_id` query parameter, which the URL signature covers, and echoes it on the execute-path response (`TransactionResponse.agent_identity_hash`, produced directly by the Exchange — the Broker is not in the execute response path for this field). +- A capable delivery endpoint (edge function) verifies the binding **fully offline**: verify the URL's Ed25519 signature against the Exchange's published public key (proves the hash is Exchange-issued and untampered), then require the fetcher to present its public key and an RFC 9421 signature over the retrieval request, and check `thumbprint(presented key) == agent_identity_hash`. No JWKS fetch required. - Bound to the agent's request-signing key, never to the principal/delegation — the agent is the fetcher, and is exactly one key per transaction. -- Enforcement is **NOT mandatory**: a bearer-only signed-URL CDN that cannot run code falls back to HMAC + short TTL + TLS. RAMP reference implementations run on edge functions and **do** enforce it. +- Enforcement is **NOT mandatory**: a CDN that cannot run code uses its native signed-URL scheme (a CloudFront RSA canned policy, for example) + short TTL + TLS. RAMP reference implementations run on edge functions and **do** enforce it. See [Signed URL Verification](/components/edge-function/signed-url-verification) for the full edge-function verification flow and the stolen-URL threat analysis. diff --git a/website/src/content/docs/protocol/scenario-walkthrough.mdx b/website/src/content/docs/protocol/scenario-walkthrough.mdx index 2aab52be..98dca367 100644 --- a/website/src/content/docs/protocol/scenario-walkthrough.mdx +++ b/website/src/content/docs/protocol/scenario-walkthrough.mdx @@ -51,7 +51,7 @@ Before any agent request, providers configure their domains and Exchanges ingest } ``` -**Edge function** — deployed on Hearst's CDN (CloudFront/Akamai/Fastly). Blocks unauthorized AI bots with 403 + `X-Content-Rules` header pointing to the Exchange. Verifies HMAC-SHA256 signed URLs for authorized requests. +**Edge function** — deployed on Hearst's CDN (Cloudflare/Fastly/Lambda@Edge). Blocks unauthorized AI bots with 403 + `X-Content-Rules` header pointing to the Exchange. Verifies Ed25519 signed URLs for authorized requests, using the Exchange's published public key. ### 0b. Agent Key Announcement @@ -756,17 +756,17 @@ The request body is identical to the direct case; the forwarding chain lives in reporting_required: true, reporting_deadline: "2026-03-16T15:00:00Z" } -9. Sign URL (HMAC-SHA256 — Exchange<->CDN shared secret, separate from Ed25519 offer signing): - baseURL = "https://cdn.techcrunch.com/server/premium/ai-regulation-2026.html" - canonical = baseURL + "\n" + expires + "\n" + agent_id + "\n" + txn_id - sig = HMAC-SHA256(canonical, shared_secret) +9. Sign the delivery URL (Ed25519, tenant signing key — a different key from the + Ed25519 offer signing key, verified by a different party): + canonical = "GET\n" + + sig = base64url(Ed25519-sign(canonical, tenant private key)) Signed URL: https://cdn.techcrunch.com/server/premium/ai-regulation-2026.html - ?expires=1710517800 - &agent_id=e3b0c442... - &txn_id=txn-alpha-001 - &sig= + ?agent_id=e3b0c442... + &exp=1710517800 + &kid=vJ3xR1mQ... + &sig=k7Qm2xR9vT4nB8aL... 10. Create reporting obligation: due by 2026-03-16T15:00:00Z ``` @@ -782,7 +782,7 @@ The request body is identical to the direct case; the forwarding chain lives in "transaction_id": "txn-alpha-001", "billing_id": "bill-sub-alpha-001", "resource_title": "AI Regulation: What Providers Need to Know", - "retrieval_endpoint": "https://cdn.techcrunch.com/server/premium/ai-regulation-2026.html?expires=1710517800&agent_id=e3b0c442...&txn_id=txn-alpha-001&sig=d4e5f6a7b8c9...", + "retrieval_endpoint": "https://cdn.techcrunch.com/server/premium/ai-regulation-2026.html?agent_id=e3b0c442...&exp=1710517800&kid=vJ3xR1mQ...&sig=k7Qm2xR9vT4nB8aL...", "cost": { "amount": "0", "currency": "USD", "unit_cost": "0" }, "delivery_method": "DELIVERY_METHOD_INSTRUCTIONS", "subscription_id": "SUB-ANTHROPIC-HEARST-2026", @@ -802,35 +802,36 @@ The `subscription_unit_value` field carries the per-unit cost ($0.05) even thoug --- -## Phase 4: Content Fetch (HMAC Signed URL, Agent Identity Binding) +## Phase 4: Content Fetch (Ed25519 Signed URL, Agent Identity Binding) ``` Agent -> CDN: GET https://cdn.techcrunch.com/server/premium/ai-regulation-2026.html - ?expires=1710517800 - &agent_id=e3b0c442... - &txn_id=txn-alpha-001 - &sig=d4e5f6a7b8c9... - Header: X-Agent-Id: claude-agent-001 - Header: X-Agent-Domain: claude.ai + ?agent_id=e3b0c442... + &exp=1710517800 + &kid=vJ3xR1mQ... + &sig=k7Qm2xR9vT4nB8aL... + Header: X-RAMP-Agent-Key: + Header: Signature-Input: sig1=("@method" "@target-uri");keyid=...;alg="ed25519";created=...;expires=... + Header: Signature: sig1=:: Edge Function: - 1. Has signed URL params? YES - 2. Verify HMAC-SHA256 signature: - canonical = "https://cdn.techcrunch.com/server/premium/ai-regulation-2026.html\n1710517800\ne3b0c442...\ntxn-alpha-001" - HMAC-SHA256(canonical, shared_secret) == sig param? -> PASS - 3. Check expiry: 1710517800 > now? -> PASS (URL valid for 5 minutes) - 4. Check agent identity binding: - SHA256(requester.id + ":" + requester.domain) == "e3b0c442..."? -> PASS - (Prevents one agent from using another agent's signed URL) - 5. Pass through -> CDN serves content from S3/origin + 1. Has a sig param? YES + 2. Resolve kid to a public key from the Exchange key directory (cached) + 3. Verify the Ed25519 signature over "GET\n" -> PASS + 4. Check expiry: exp > now? -> PASS (URL valid for 5 minutes) + 5. Check agent identity binding: + thumbprint(presented public key) == agent_id? -> PASS + Verify the RFC 9421 signature with that key (proof of possession) -> PASS + (The agent must hold the private half; presenting the public key is not enough) + 6. Strip agent_id, exp, kid, sig -> CDN serves content from S3/origin CDN access log records: - - URL with all params (txn_id, agent_id, sig) + - URL with all params (agent_id, exp, kid, sig) - Client IP, timestamp, bytes transferred, HTTP 200 - This log is controlled by the provider (Hearst) and used for reconciliation ``` -**HMAC canonicalization format**: The signed URL uses `\n` (newline) as the delimiter between fields in the canonical string. The order is: `baseURL\nexpires\nagent_id\ntxn_id`. This is an Exchange-CDN concern (symmetric HMAC-SHA256 shared secret), entirely separate from the Ed25519 asymmetric signatures used for offer signing and request authentication. +**Canonical message**: the literal `GET`, one newline, then the whole URL with the `sig` parameter removed and the remaining query sorted by key — so scheme, host, path and every other parameter are covered. The delivery-URL signature and the offer signature are both Ed25519; they use different keys and are verified by different parties. See [Signed URL Verification](/components/edge-function/signed-url-verification). --- @@ -898,12 +899,13 @@ A successful `UsageReportResponse` carries only the Exchange-assigned `report_id Three independent records that must agree: ``` -1. CDN access log (Hearst controls): - - txn_id=txn-alpha-001 fetched at 15:25:00, 200 OK, 45KB - - agent_id=e3b0c442..., sig verified by edge function +1. Edge delivery log (Hearst controls): + - url_hash=9f2c7a4e... fetched at 15:25:00, 200 OK, 45KB + - agent_id=e3b0c442..., signature and agent binding verified by the edge 2. Exchange transaction log (SSP-Alpha controls): - - txn-alpha-001: subscription SUB-ANTHROPIC-HEARST-2026, amount=$0 + - txn-alpha-001: signed_url_hash=9f2c7a4e... (the join key to the log above) + - subscription SUB-ANTHROPIC-HEARST-2026, amount=$0 - subscription_unit_value: $0.05 (value of the access for accounting) - offer_snapshot: article "AI Regulation...", 3300 est tokens - offer_snapshot includes exchange_signature (Ed25519, non-repudiable) diff --git a/website/src/content/docs/protocol/transaction-flow.mdx b/website/src/content/docs/protocol/transaction-flow.mdx index 81064588..f2b4ad9d 100644 --- a/website/src/content/docs/protocol/transaction-flow.mdx +++ b/website/src/content/docs/protocol/transaction-flow.mdx @@ -342,7 +342,7 @@ Each item's `offer` is the full signed Offer reflected back exactly as received } ``` -The `retrieval_endpoint` is a CDN signed URL bound to `agent_identity_hash`. A bearer-only CDN serves it on signature + expiry alone (HMAC + short TTL + TLS); a capable edge function additionally requires the agent to present its public key and an RFC 9421 signature, verifying the binding offline. See [Signed URL Verification](/components/edge-function/signed-url-verification). +The `retrieval_endpoint` is a CDN signed URL bound to `agent_identity_hash`. A bearer-only CDN serves it on signature + expiry alone (the URL signature + short TTL + TLS); a capable edge function additionally requires the agent to present its public key and an RFC 9421 signature, verifying the binding offline. See [Signed URL Verification](/components/edge-function/signed-url-verification). ### What Happens Inside the Exchange @@ -352,7 +352,7 @@ The `retrieval_endpoint` is a CDN signed URL bound to `agent_identity_hash`. A b 4. Verify offer signature (reconstruct offer from signed token) 5. Authorize billing (`BillingAdapter.Authorize`) -- or skip for subscription offers 6. Write transaction to WAL (must succeed before signing URL) -7. Generate HMAC-SHA256 signed URL (Exchange-CDN shared secret) +7. Generate the Ed25519 signed delivery URL (or a CloudFront RSA canned policy, per the tenant's signing scheme) 8. Create reporting obligation with deadline ### Batch TransactionRequest diff --git a/website/src/content/docs/protocol/walkthrough-academic.mdx b/website/src/content/docs/protocol/walkthrough-academic.mdx index fe761789..189924e9 100644 --- a/website/src/content/docs/protocol/walkthrough-academic.mdx +++ b/website/src/content/docs/protocol/walkthrough-academic.mdx @@ -706,7 +706,7 @@ The Exchange processes all 49 items in a single batch: 4. **Balance check** — confirms `litrev-agent@cs.stanford.edu` has sufficient balance ($25.50 for 17 paid items). 5. **Quota check** — confirms 49 accesses fit within the subscription's remaining quota (76,520). 6. **WAL write** — records all 49 transactions in the write-ahead log before generating signed URLs (write-before-sign invariant). -7. **Signed URL generation** — generates 49 HMAC-SHA256 signed URLs, each binding `agent_id` and `txn_id`. +7. **Signed URL generation** — generates 49 Ed25519 signed URLs, each binding `agent_id`. Response (abbreviated — showing one item from each category): @@ -718,7 +718,7 @@ Response (abbreviated — showing one item from each category): "offer_id": "c2e59d05-f026-4f84-9c97-67906b71a0ce", "transaction_id": "txn-arxiv-001", "billing_id": "bill-arxiv-001", - "retrieval_endpoint": "https://cdn.arxiv.org/ramp/2406.11838.pdf?expires=1742478600&agent_id=3c8f2a1d...&txn_id=txn-arxiv-001&sig=hmac-sha256-a1b2c3...", + "retrieval_endpoint": "https://cdn.arxiv.org/ramp/2406.11838.pdf?agent_id=3c8f2a1d...&exp=1742478600&kid=pQ7wL2xB...&sig=a1b2c3...", "cost": { "amount": "0", "currency": "USD" @@ -735,7 +735,7 @@ Response (abbreviated — showing one item from each category): "offer_id": "ba9b114a-1e5b-473c-886c-4b9ac9b89a9d", "transaction_id": "txn-elsevier-sub-001", "billing_id": "bill-elsevier-sub-001", - "retrieval_endpoint": "https://cdn.sciencedirect.com/ramp/S1361841524001250.pdf?expires=1742478600&agent_id=3c8f2a1d...&txn_id=txn-elsevier-sub-001&sig=hmac-sha256-d4e5f6...", + "retrieval_endpoint": "https://cdn.sciencedirect.com/ramp/S1361841524001250.pdf?agent_id=3c8f2a1d...&exp=1742478600&kid=pQ7wL2xB...&sig=d4e5f6...", "cost": { "amount": "0", "currency": "USD" @@ -757,7 +757,7 @@ Response (abbreviated — showing one item from each category): "offer_id": "92881d37-e18d-48d1-993b-51e4e5cea630", "transaction_id": "txn-springer-paid-001", "billing_id": "bill-springer-paid-001", - "retrieval_endpoint": "https://cdn.nature.com/ramp/s41592-024-02401-5.pdf?expires=1742478600&agent_id=3c8f2a1d...&txn_id=txn-springer-paid-001&sig=hmac-sha256-g7h8i9...", + "retrieval_endpoint": "https://cdn.nature.com/ramp/s41592-024-02401-5.pdf?agent_id=3c8f2a1d...&exp=1742478600&kid=pQ7wL2xB...&sig=g7h8i9...", "cost": { "amount": "1.50", "currency": "USD" @@ -794,7 +794,7 @@ Response (abbreviated — showing one item from each category): - **`total_cost`**: $25.50 for all 49 papers. The 12 free OA papers and 20 subscription papers cost $0.00 each. - **`subscription_unit_value`**: $1.50 on each subscription item. This is the imputed value for accounting purposes (ASC 606 prepaid drawdown). Stanford's subscription is "spending" this value even though the marginal cost is zero. - **`subscription_quota`**: After the transaction, 76,500 accesses remain (20 were consumed from the previous 76,520). The quota resets on April 1. -- **Signed URLs**: Each URL is valid for 5 minutes (`expires_at`), contains the `agent_identity_hash` to prevent URL sharing, and embeds the `txn_id` for three-sided reconciliation (agent, Exchange, CDN). +- **Signed URLs**: Each URL is valid for 5 minutes (`expires_at`), contains the `agent_identity_hash` to prevent URL sharing, and is recorded by its hash for three-sided reconciliation (agent, Exchange, CDN). The agent then fetches all 49 PDFs via the signed URLs. For each paper, because `resource_mutability` is `RESOURCE_MUTABILITY_STATIC`, the agent computes SHA-256 of the delivered PDF and compares against the `identity.content_hash` from the offer. A mismatch would be grounds for a `DisputeTransaction`. @@ -879,7 +879,7 @@ Response: - **Agent to Exchange**: signed with an RFC 9421 HTTP Message Signature (alg=ed25519) in the HTTP headers - **Institutional delegation**: delegation JWT signed by `stanford.edu`'s Ed25519 key, holder-of-key bound via `cnf.jkt` (offline-verifiable) - **Exchange to Agent**: `exchange_signature` on each Offer (Ed25519, stateless verification) -- **Signed URLs**: HMAC-SHA256 (Exchange-CDN shared secret, agent identity bound) +- **Signed URLs**: Ed25519 (Exchange-signed, publicly verifiable, agent identity bound) - **Content integrity**: SHA-256 hash verification on all 49 delivered PDFs (`RESOURCE_MUTABILITY_STATIC`) **Key academic-specific capabilities demonstrated:** diff --git a/website/src/content/docs/protocol/walkthrough-credit-report.mdx b/website/src/content/docs/protocol/walkthrough-credit-report.mdx index 74cf1d4f..c8bf156b 100644 --- a/website/src/content/docs/protocol/walkthrough-credit-report.mdx +++ b/website/src/content/docs/protocol/walkthrough-credit-report.mdx @@ -451,7 +451,7 @@ The Exchange performs: 2. **Agent authentication** — verifies the request's RFC 9421 HTTP Message Signature (alg=ed25519, carried in HTTP headers) against the agent's public key published in the WBA directory at `legalcorp.com/.well-known/http-message-signatures-directory`. 3. **Balance check** — confirms `due-diligence-bot@legalcorp.com` has sufficient balance ($61.99). 4. **WAL write** — records the transaction in the write-ahead log before generating the signed URL (write-before-sign invariant). -5. **Signed URL generation** — HMAC-SHA256 over `(base_url, expires, agent_id, txn_id)` using the Exchange-CDN shared secret. +5. **Signed URL generation** — Ed25519 over `"GET\n" + ` with the tenant's signing key. The delivery endpoint verifies it with the Exchange's published public key. Response: @@ -463,7 +463,7 @@ Response: { "transaction_id": "txn-dnb-acme-001", "billing_id": "bill-dnb-acme-001", - "retrieval_endpoint": "https://api.dnb.com/ramp/deliver/123456789/standard?expires=1742475900&agent_id=9c5f0d3e...&txn_id=txn-dnb-acme-001&sig=hmac-sha256-b8c9d0e1...", + "retrieval_endpoint": "https://api.dnb.com/ramp/deliver/123456789/standard?agent_id=9c5f0d3e...&exp=1742475900&kid=hT4nR8yK...&sig=b8c9d0e1...", "cost": { "amount": "61.99", "currency": "USD" @@ -489,14 +489,14 @@ The signed URL is valid for 5 minutes. The `agent_identity_hash` (the agent's RF The agent fetches the credit report via the signed URL: ```bash -GET https://api.dnb.com/ramp/deliver/123456789/standard?expires=1742475900&agent_id=9c5f0d3e...&txn_id=txn-dnb-acme-001&sig=hmac-sha256-b8c9d0e1... +GET https://api.dnb.com/ramp/deliver/123456789/standard?agent_id=9c5f0d3e...&exp=1742475900&kid=hT4nR8yK...&sig=b8c9d0e1... ``` D&B's delivery endpoint verifies: -1. HMAC-SHA256 signature matches (Exchange-CDN shared secret) +1. The Ed25519 signature verifies against the Exchange's published public key 2. URL has not expired 3. `agent_id` in URL matches the requesting agent's identity hash -4. `txn_id` is recorded for three-sided reconciliation +4. The signed-URL hash is recorded for three-sided reconciliation The agent receives the credit report as JSON: diff --git a/website/src/content/docs/protocol/walkthrough-due-diligence.mdx b/website/src/content/docs/protocol/walkthrough-due-diligence.mdx index 99086454..07123114 100644 --- a/website/src/content/docs/protocol/walkthrough-due-diligence.mdx +++ b/website/src/content/docs/protocol/walkthrough-due-diligence.mdx @@ -652,7 +652,7 @@ The Broker sends batch `TransactionRequest` to each Exchange. One request per Ex "offer_id": "e94e65fc-293e-48bd-9f35-8b38345944d1", "transaction_id": "txn-legal-001", "billing_id": "bill-legal-001", - "retrieval_endpoint": "https://cdn.legaldata.com/pacer/24cv01234.pdf?expires=1742486700&agent_id=3a2b1c0d...&txn_id=txn-legal-001&sig=hmac-sha256-f1e2d3...", + "retrieval_endpoint": "https://cdn.legaldata.com/pacer/24cv01234.pdf?agent_id=3a2b1c0d...&exp=1742486700&kid=mZ6vC3jD...&sig=f1e2d3...", "cost": { "amount": "4.50", "currency": "USD" }, "delivery_method": "DELIVERY_METHOD_INSTRUCTIONS", "expires_at": "2026-03-20T14:15:00Z" @@ -661,7 +661,7 @@ The Broker sends batch `TransactionRequest` to each Exchange. One request per Ex "offer_id": "b570b9a9-3bd8-47ed-a580-c3245981a573", "transaction_id": "txn-legal-002", "billing_id": "bill-legal-002", - "retrieval_endpoint": "https://cdn.legaldata.com/pacer/25cv05678.pdf?expires=1742486700&agent_id=3a2b1c0d...&txn_id=txn-legal-002&sig=hmac-sha256-a4b5c6...", + "retrieval_endpoint": "https://cdn.legaldata.com/pacer/25cv05678.pdf?agent_id=3a2b1c0d...&exp=1742486700&kid=mZ6vC3jD...&sig=a4b5c6...", "cost": { "amount": "3.20", "currency": "USD" }, "delivery_method": "DELIVERY_METHOD_INSTRUCTIONS", "expires_at": "2026-03-20T14:15:00Z" @@ -670,7 +670,7 @@ The Broker sends batch `TransactionRequest` to each Exchange. One request per Ex "offer_id": "a37467da-1ba4-4490-9072-75d7194882ea", "transaction_id": "txn-legal-003", "billing_id": "bill-legal-003", - "retrieval_endpoint": "https://cdn.legaldata.com/pacer/23cv09012.pdf?expires=1742486700&agent_id=3a2b1c0d...&txn_id=txn-legal-003&sig=hmac-sha256-d7e8f9...", + "retrieval_endpoint": "https://cdn.legaldata.com/pacer/23cv09012.pdf?agent_id=3a2b1c0d...&exp=1742486700&kid=mZ6vC3jD...&sig=d7e8f9...", "cost": { "amount": "2.80", "currency": "USD" }, "delivery_method": "DELIVERY_METHOD_INSTRUCTIONS", "expires_at": "2026-03-20T14:15:00Z" @@ -866,7 +866,7 @@ RAMP replaces the **data acquisition** layer of due diligence, not the analysis. - **Agent to Broker**: RFC 9421 HTTP Message Signature (alg=ed25519) in HTTP headers - **Broker to Exchange**: the Broker adds its own labeled RFC 9421 HTTP Message Signature (alg=ed25519) in the HTTP headers, covering the forwarded request plus the agent's prior signature; the ordered set of labeled header signatures is the forwarding chain - **Exchange to Agent**: `exchange_signature` on every Offer (Ed25519, stateless verification) -- **Signed URLs**: HMAC-SHA256 (Exchange-CDN shared secret, agent identity bound) +- **Signed URLs**: Ed25519 (Exchange-signed, publicly verifiable, agent identity bound) ## Next Steps diff --git a/website/src/content/docs/protocol/walkthrough-eu-regulation.mdx b/website/src/content/docs/protocol/walkthrough-eu-regulation.mdx index 6484f29c..dff759af 100644 --- a/website/src/content/docs/protocol/walkthrough-eu-regulation.mdx +++ b/website/src/content/docs/protocol/walkthrough-eu-regulation.mdx @@ -391,7 +391,7 @@ Response: { "transaction_id": "txn-eurlex-001", "billing_id": "bill-eurlex-001", - "retrieval_endpoint": "https://cdn.eurlex.europa.eu/legal-content/EN/TXT/XML/?uri=CELEX:32024R1689&expires=1742404200&agent_id=3e8f1a2b...&txn_id=txn-eurlex-001&sig=hmac-sha256-e7f8a9...", + "retrieval_endpoint": "https://cdn.eurlex.europa.eu/legal-content/EN/TXT/XML/?agent_id=3e8f1a2b...&exp=1742404200&kid=gY2kP7mV...&sig=e7f8a9...&uri=CELEX:32024R1689", "cost": { "amount": "0", "currency": "EUR" @@ -409,7 +409,7 @@ Response: } ``` -Cost is zero. The signed URL still includes agent identity binding and HMAC verification -- even free content flows through the standard transaction pipeline for auditability. +Cost is zero. The signed URL still includes agent identity binding and signature verification -- even free content flows through the standard transaction pipeline for auditability. --- @@ -418,9 +418,10 @@ Cost is zero. The signed URL still includes agent identity binding and HMAC veri The agent fetches via the signed URL: ``` -GET https://cdn.eurlex.europa.eu/legal-content/EN/TXT/XML/?uri=CELEX:32024R1689 - &expires=1742404200&agent_id=3e8f1a2b...&txn_id=txn-eurlex-001 - &sig=hmac-sha256-e7f8a9... +GET https://cdn.eurlex.europa.eu/legal-content/EN/TXT/XML/ + ?agent_id=3e8f1a2b...&exp=1742404200&kid=gY2kP7mV... + &sig=e7f8a9... + &uri=CELEX:32024R1689 ``` EUR-Lex delivers the regulation as **Akoma Ntoso XML** -- the OASIS standard for legislative documents. Because `resource_mutability` is `STATIC`, the agent verifies the SHA-256 hash against `identity.content_hash`. A match confirms this is the exact Official Journal text as published on 12 July 2024. diff --git a/website/src/content/docs/protocol/walkthrough-medical-imaging.mdx b/website/src/content/docs/protocol/walkthrough-medical-imaging.mdx index e9c689ac..1afb568d 100644 --- a/website/src/content/docs/protocol/walkthrough-medical-imaging.mdx +++ b/website/src/content/docs/protocol/walkthrough-medical-imaging.mdx @@ -307,7 +307,7 @@ The error includes an actionable URL (in `metadata`) where the institution can a "offer_id": "78e43486-599b-4016-b5a7-1bed5d22d1c0", "transaction_id": "txn-mri-gbm-0152-001", "billing_id": "bill-mri-001", - "retrieval_endpoint": "https://cdn.medimg-exchange.com/delivery/manifest/txn-mri-gbm-0152-001.json?expires=1742410800&agent_id=c4d5e6f7...&sig=hmac-sha256-9a8b7c...", + "retrieval_endpoint": "https://cdn.medimg-exchange.com/delivery/manifest/txn-mri-gbm-0152-001.json?agent_id=c4d5e6f7...&exp=1742410800&kid=sW9bF5tN...&sig=9a8b7c...", "cost": { "amount": "25.00", "currency": "USD" @@ -346,7 +346,7 @@ The error includes an actionable URL (in `metadata`) where the institution can a The agent fetches the delivery manifest via the signed URL: ```bash -GET https://cdn.medimg-exchange.com/delivery/manifest/txn-mri-gbm-0152-001.json?expires=1742410800&agent_id=c4d5e6f7...&sig=hmac-sha256-9a8b7c... +GET https://cdn.medimg-exchange.com/delivery/manifest/txn-mri-gbm-0152-001.json?agent_id=c4d5e6f7...&exp=1742410800&kid=sW9bF5tN...&sig=9a8b7c... ``` The manifest is a JSON document listing all 847 DICOM instances grouped by series, each with its own signed URL: @@ -369,14 +369,14 @@ The manifest is a JSON document listing all 847 DICOM instances grouped by serie { "sop_instance_uid": "1.2.840.113619.2.388.10180.7.2026.3.14.8.12.42.1.1", "instance_number": 1, - "url": "https://cdn.medimg-exchange.com/dicom/txn-mri-gbm-0152-001/series-1/instance-001.dcm?sig=hmac-sha256-...", + "url": "https://cdn.medimg-exchange.com/dicom/txn-mri-gbm-0152-001/series-1/instance-001.dcm?exp=1742410800&kid=sW9bF5tN...&sig=4c1d8e...", "size_bytes": 1524736, "hash": "sha256:e5f6a7b8..." }, { "sop_instance_uid": "1.2.840.113619.2.388.10180.7.2026.3.14.8.12.42.1.2", "instance_number": 2, - "url": "https://cdn.medimg-exchange.com/dicom/txn-mri-gbm-0152-001/series-1/instance-002.dcm?sig=hmac-sha256-...", + "url": "https://cdn.medimg-exchange.com/dicom/txn-mri-gbm-0152-001/series-1/instance-002.dcm?exp=1742410800&kid=sW9bF5tN...&sig=7f2a9b...", "size_bytes": 1524736, "hash": "sha256:c9d0e1f2..." } diff --git a/website/src/content/docs/protocol/walkthrough-v1.mdx b/website/src/content/docs/protocol/walkthrough-v1.mdx index 9388f505..8e53e7e1 100644 --- a/website/src/content/docs/protocol/walkthrough-v1.mdx +++ b/website/src/content/docs/protocol/walkthrough-v1.mdx @@ -231,7 +231,7 @@ The Exchange performs: 2. **Agent authentication** — verifies the request's RFC 9421 HTTP Message Signature against the agent's published public key. 3. **Balance check** — confirms `research-bot@startup.com` has sufficient balance ($0.05). 4. **WAL write** — records the transaction in the write-ahead log before generating the signed URL (write-before-sign invariant). -5. **Signed URL generation** — HMAC-SHA256 over `(base_url, expires, agent_id, txn_id)` using the Exchange-CDN shared secret. +5. **Signed URL generation** — Ed25519 over `"GET\n" + ` with the tenant's signing key. The delivery endpoint verifies it with the Exchange's published public key. Response: @@ -243,7 +243,7 @@ Response: "offer_id": "3a1cf96e-74b7-4c5d-abb0-925c3f83dfe3", "transaction_id": "txn-tc-001", "billing_id": "bill-tc-001", - "retrieval_endpoint": "https://cdn.techcrunch.com/premium/ai-agents-commerce.html?expires=1742403900&agent_id=7a3f8c1d...&txn_id=txn-tc-001&sig=hmac-sha256-d4e5f6...", + "retrieval_endpoint": "https://cdn.techcrunch.com/premium/ai-agents-commerce.html?agent_id=7a3f8c1d...&exp=1742403900&kid=vJ3xR1mQ...&sig=d4e5f6...", "cost": { "amount": "0.05", "currency": "USD" @@ -268,14 +268,14 @@ The signed URL is valid for 5 minutes. The `agent_identity_hash` (the agent's RF The agent fetches the content via the signed URL: ```bash -GET https://cdn.techcrunch.com/premium/ai-agents-commerce.html?expires=1742403900&agent_id=7a3f8c1d...&txn_id=txn-tc-001&sig=hmac-sha256-d4e5f6... +GET https://cdn.techcrunch.com/premium/ai-agents-commerce.html?agent_id=7a3f8c1d...&exp=1742403900&kid=vJ3xR1mQ...&sig=d4e5f6... ``` The CDN edge function verifies: -1. HMAC-SHA256 signature matches (Exchange-CDN shared secret) +1. The Ed25519 signature verifies against the Exchange's published public key 2. URL has not expired 3. `agent_id` in URL matches the requesting agent's identity hash -4. `txn_id` is recorded for three-sided reconciliation +4. The signed-URL hash is recorded for three-sided reconciliation **Content hash verification**: Because `resource_mutability` is `STATIC`, the agent computes SHA-256 of the delivered HTML and compares it against `identity.content_hash` from the offer. Match confirms the content is exactly what was promised. Mismatch would be grounds for a `DisputeTransaction`. @@ -525,7 +525,7 @@ The remaining steps follow the same pattern as Scenario A, with one key differen "offer_id": "c2301145-0f74-43af-be4e-eb8cd2a988be", "transaction_id": "txn-bb-001", "billing_id": "bill-bb-sub-001", - "retrieval_endpoint": "https://cdn.marketdata.example.com/earnings/NVDA/2026-Q1-transcript.html?expires=1742404200&agent_id=8b4f9d2e...&txn_id=txn-bb-001&sig=hmac-sha256-a7b8c9...", + "retrieval_endpoint": "https://cdn.marketdata.example.com/earnings/NVDA/2026-Q1-transcript.html?agent_id=8b4f9d2e...&exp=1742404200&kid=vJ3xR1mQ...&sig=a7b8c9...", "cost": { "amount": "0", "currency": "USD" @@ -620,7 +620,7 @@ result, err := client.Fetch(ctx, url) - **Agent → Exchange**: RFC 9421 HTTP Message Signature (Ed25519 over the HTTP request — method, target URI, and body content digest — in the `Signature` / `Signature-Input` headers) - **Delegation**: delegation JWT signed by principal's Ed25519 key, holder-of-key bound via `cnf.jkt` (offline-verifiable) - **Exchange → Agent**: `exchange_signature` on Offer (Ed25519, stateless verification) -- **Signed URLs**: HMAC-SHA256 (Exchange-CDN shared secret, agent identity bound) +- **Signed URLs**: Ed25519 (Exchange-signed, publicly verifiable, agent identity bound) ## Next Steps diff --git a/website/src/content/docs/security/threat-model.mdx b/website/src/content/docs/security/threat-model.mdx index e2e3b068..1c431380 100644 --- a/website/src/content/docs/security/threat-model.mdx +++ b/website/src/content/docs/security/threat-model.mdx @@ -42,7 +42,7 @@ This single pattern accounts for most ad-tech fraud: domain spoofing (self-repor ### T5: Provider revenue skimming **Attack**: Exchange tells provider 1,000 transactions occurred; actually processed 1,500. **Countermeasure (reconciliation)**: **Three-sided reconciliation.** Provider's CDN logs, Exchange's transaction log, and usage reports must all agree. -**Countermeasure (protocol)**: Signed URLs include transaction_id in URL parameters for CDN-to-Exchange reconciliation. +**Countermeasure (protocol)**: Both sides record `signed_url_hash` — SHA-256 of the delivery URL verbatim. The Exchange stores it on the transaction and the delivery endpoint stores it on its delivery event, so the two logs join on a value neither side chooses. ### T6: Overly long signed URL expiry **Attack**: Exchange issues 24-hour URLs instead of 5-minute, enabling reuse. @@ -50,13 +50,13 @@ This single pattern accounts for most ad-tech fraud: domain spoofing (self-repor ### T7: Signing key leakage **Attack**: Exchange shares signing keys with a preferred requester, enabling URL forgery. -**Countermeasure (protocol)**: **Asymmetric signing.** Provider holds the private key; Exchange requests signed URLs from provider's signing service. +**Countermeasure (protocol)**: **Asymmetric signing, with per-tenant key isolation.** The Exchange holds a separate private key per tenant in a secrets manager and signs there; the delivery endpoint verifies with the matching public key, published in the Exchange's key directory. A leaked verification key forges nothing, and a compromised edge cannot mint URLs, because no signing capability is ever distributed. Sharing a signing key with one requester would require exporting a tenant private key, which no code path does. ## 2. Demand-Side Threats (Dishonest Agent / Requester) ### T8: Signed URL sharing **Attack**: Agent A pays, shares signed URL with Agents B, C, D before expiry. -**Countermeasure (protocol)**: **Agent-bound signed URLs.** Embed the agent's RFC 7638 JWK Thumbprint (`agent_identity_hash`) in the HMAC-signed URL; a capable edge function requires the fetcher to present its public key + an RFC 9421 signature (proof-of-possession) and checks `thumbprint(presented key) == agent_identity_hash`. Implemented via `agent_identity_hash` on `TransactionResponse`. +**Countermeasure (protocol)**: **Agent-bound signed URLs.** Embed the agent's RFC 7638 JWK Thumbprint (`agent_identity_hash`) as the URL's `agent_id` parameter, covered by the URL's Ed25519 signature; a capable edge function requires the fetcher to present its public key + an RFC 9421 signature (proof-of-possession) and checks `thumbprint(presented key) == agent_identity_hash`. Implemented via `agent_identity_hash` on `TransactionResponse`. ### T9: Systematic caching / content mirroring **Attack**: Agent fetches every article once, caches locally forever, never pays again. From bd8f24a465a1ed712ba4ebce15eea9dd1f0be2a9 Mon Sep 17 00:00:00 2001 From: noxlesh Date: Tue, 1 Sep 2026 17:39:53 +0300 Subject: [PATCH 06/16] docs(ci): deny the retired HMAC signed-URL phrasings The wording was corrected everywhere at once in the preceding commits; this stops it coming back, the same way the retired JWS phrasings are held out. Two patterns are deliberately narrower than the defect they guard. The bare token HMAC stays legal. Report tokens are genuinely HMAC-hashed, and a future document may legitimately cite HMAC as a rejected alternative -- banning the token would force a later author to write around a true statement. The bare token txn_id also stays legal, and this one was found by running the check rather than by reasoning about it: three Go samples in request-flows.mdx use txn_id as a structured log field name, which is correct and unrelated to URL parameters. The patterns are anchored to the URL forms instead. Each pattern was verified by breaking the tree and watching the gate fail, not by observing that it passes. That caught a real defect in this commit: the canonicalization pattern was written with one character too many and matched nothing, so it would have shipped as a guard that could never fire. --- scripts/check-doc-conformance.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scripts/check-doc-conformance.sh b/scripts/check-doc-conformance.sh index d31d5698..693eaccd 100755 --- a/scripts/check-doc-conformance.sh +++ b/scripts/check-doc-conformance.sh @@ -102,6 +102,22 @@ patterns=( 'JWS \(alg=EdDSA\)' 'JWS \(EdDSA\)' 'JWS / Ed25519' 'EdDSA via JWS' 'JWS EdDSA signature' 'JWS Compact Serialization on Offer' 'JWS for content signatures' 'Offer JWS' 'JWS alg' + # Delivery URLs are signed with Ed25519 by the Exchange -- a base64url-no-pad + # signature over "GET\n" -- and verified with its published + # PUBLIC key; the CloudFront-native path uses RSA, verified by the CDN. No + # implementation ever used HMAC and no secret ever reaches a delivery endpoint. + # These are the retired phrasings of that claim. The bare token "HMAC" stays + # legal: report tokens are genuinely HMAC-hashed, and a doc may cite HMAC as a + # rejected alternative. + 'HMAC-SHA256 signed URL' 'HMAC signed URL' 'HMAC-signed URL' 'URL HMAC' + 'HMAC canonicalization' 'HMAC [Cc]anonicalization' 'hmac-sha256-' + 'HMAC secret' 'hmacSecret' 'HMAC_SECRET' 'CDNGenericHMAC' 'Generic HMAC' + 'Exchange.CDN shared secret' 'shared secret between Exchange and CDN' + # The signed URL carries no transaction-id parameter and no `expires`; the + # parameters are agent_id, exp, kid and sig, and reconciliation joins on + # signed_url_hash. Anchored to the URL forms so the patterns do NOT collide + # with the legitimate slog field name `txn_id` in Go samples. + '&txn_id=' '?txn_id=' 'txn_id=txn-' 'baseURL.nexpires' ) # Files where naming a removed identifier is legitimate (they record history). From 631b8d66bfdc6514c4d4cbeb43b83b5470b0d1fc Mon Sep 17 00:00:00 2001 From: noxlesh Date: Tue, 1 Sep 2026 17:42:27 +0300 Subject: [PATCH 07/16] docs(changelog): record the signed-URL scheme correction Exhaustive entry in the protocol changelog, curated mirror on the website, both under Unreleased and following the format of the offer-signature correction that shipped earlier in this release -- the same defect class, documented the same way. The v1.0 history is left as written. Its retrieval-URL binding entry is scheme-neutral and remains accurate. --- proto/CHANGELOG.md | 52 +++++++++++++++++++ .../src/content/docs/reference/changelog.mdx | 28 ++++++++++ 2 files changed, 80 insertions(+) diff --git a/proto/CHANGELOG.md b/proto/CHANGELOG.md index fb7ea1c9..8a391f0e 100644 --- a/proto/CHANGELOG.md +++ b/proto/CHANGELOG.md @@ -2,6 +2,58 @@ ## Unreleased +**Signed delivery URLs are documented as Ed25519 signed by the Exchange and +verified with its published public key, not HMAC-SHA256 over a shared secret +(documentation correction; no wire change).** Since the initial public snapshot +the file header, the DomainVerificationConfirmation comments and twenty website +pages described a symmetric scheme with a secret shared between the Exchange and +the CDN. No implementation ever produced one: `git grep -ci hmac -- sdk/` finds +nothing, and signing has always been a detached Ed25519 signature that a +delivery endpoint verifies with a public key. + +The divergence was wider than the algorithm name, so an implementer following +the documentation got four things wrong at once. The signed message is `"GET\n"` +followed by the canonical URL -- the whole URL with the `sig` parameter removed +and the remaining query sorted by key -- not four selected fields joined by +newlines, which means scheme, host, path and every publisher query parameter are +covered too. The signature is base64url with no padding, not a hex digest. The +expiry parameter is `exp`, documented as `expires`. There is a `kid` parameter +the pages never mentioned, and there is no `txn_id` parameter at all. + +Where the documentation said `txn_id` enables three-sided reconciliation, the +join key is `signed_url_hash` -- SHA-256 of the URL verbatim -- recorded by the +Exchange on the transaction and by the delivery endpoint on its delivery event. +Neither side chooses the value. + +`DomainVerificationConfirmation.signing_key` said key format is "PEM for +CloudFront, hex for HMAC"; hex is an HMAC-secret encoding, so the sentence had no +reading that matched the implementation. Its format now follows `cdn_type`, whose +documented value set becomes `"edge-ed25519"` | `"cloudfront"`. The retired +values named vendors rather than schemes, which made `"fastly"` actively wrong -- +a Fastly Compute deployment runs the Ed25519 verifier. The name `edge-ed25519` +is not new; it is the one ADR-012 assigned for this path. The value list also +moves into the field's leading comment, because the reference page renders +leading comments in preference to trailing ones and this field already had one, +so the trailing list was invisible to every reader. + +Two security claims were corrected beyond the primitive. The threat model said +the provider holds the URL-signing private key and the Exchange calls a provider +signing service; no such service exists, and the Exchange signs with a per-tenant +key it holds itself. A walkthrough verified agent binding as +`SHA256(requester.id + ":" + requester.domain)`, which binds nothing an attacker +cannot recompute; the check is a thumbprint comparison against the presented +public key plus an RFC 9421 proof of possession. + +Akamai is no longer documented as a supported delivery target: EdgeAuth verifies +with a secret shared with the CDN, which is the model this correction removes. +The retired phrasings are held out by the documentation conformance gate. + +No wire change. The `cdn_type` value set lives only in a comment on an +`optional string`, never in an enum, and the field has no consumer in the SDK, +the conformance corpus, or the reference implementation. `gen/` is regenerated +with the pinned buf; the conformance corpus is unchanged, as comments carry no +constraints. + **`Offer.offer_id` is documented as an opaque unique identifier, not a resource key (comment clarification; no wire change).** The comment already said the id is assigned by the Exchange, but an implementation historically derived it from the diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index 967195b0..f1fc893e 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -8,6 +8,34 @@ and protocol history, see [`proto/CHANGELOG.md`](https://github.com/RAMP-Protoco ## Unreleased +**Signed delivery URLs are documented as Ed25519 signed by the Exchange and +verified with its published public key, not HMAC-SHA256 over a shared secret +(documentation correction; no wire change).** Since the initial public snapshot +the proto comments and twenty website pages described a symmetric scheme with a +secret shared between the Exchange and the CDN. No implementation ever produced +one, and the SDK published from this repository has never contained an HMAC. + +An implementer following the documentation got four things wrong at once. The +signed message is `"GET\n"` followed by the canonical URL -- the whole URL with +the `sig` parameter removed and the remaining query sorted by key -- so scheme, +host, path and every publisher query parameter are covered, not four selected +fields joined by newlines. The signature is base64url with no padding, not a hex +digest. The expiry parameter is `exp`, documented as `expires`. There is a `kid` +parameter the pages never mentioned, and no `txn_id` parameter at all; +reconciliation joins on the signed-URL hash, which both the Exchange and the +delivery endpoint record. + +`DomainVerificationConfirmation.cdn_type` now documents the value set +`"edge-ed25519"` | `"cloudfront"`. The retired values named vendors rather than +schemes, which made `"fastly"` actively wrong -- a Fastly deployment runs the +Ed25519 verifier. Akamai is no longer documented as a supported delivery target, +because EdgeAuth verifies with a secret shared with the CDN. + +The signed-URL verification page is rewritten against the shipped verifier, and +now documents what it always did but never said: verification fails closed, the +signature is checked before the expiry, agent binding is enforced by default, and +the delivery endpoint holds public keys only. + **`Offer.offer_id` is documented as an opaque unique identifier, not a resource key (comment clarification; no wire change).** The comment already said the id is assigned by the Exchange, but an implementation historically derived it from the From bed71e1816c149ccad00ee583c665424e1634a65 Mon Sep 17 00:00:00 2001 From: noxlesh Date: Wed, 2 Sep 2026 11:58:38 +0300 Subject: [PATCH 08/16] docs(architecture): correct the deployment-model signed-URL claims The minimum-viable decision table named the URL parameters as Signature and Expires. The implemented parameters are sig and exp; the capitalised pair is CloudFront's own naming, which does not apply to the Ed25519 path the table describes. The same section claimed the feature set works on Fastly VCL and Akamai Property Manager rules. Neither can verify a signature, so neither can admit a paid request -- they can serve ramp.json and refuse known crawlers, and that is all. The supported-CDN table listed Akamai with the full feature set; it is not a supported target, for the reason the CDN adapters page states. The CDN adapters page still named Akamai EdgeWorkers in its frontmatter description after the section itself was replaced. --- .../src/content/docs/architecture/deployment-models.mdx | 8 ++++---- .../docs/components/edge-function/cdn-adapters.mdx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/website/src/content/docs/architecture/deployment-models.mdx b/website/src/content/docs/architecture/deployment-models.mdx index 19eb72fa..01b55ed8 100644 --- a/website/src/content/docs/architecture/deployment-models.mdx +++ b/website/src/content/docs/architecture/deployment-models.mdx @@ -89,7 +89,7 @@ The provider deploys the edge function themselves using tooling provided by RAMP | CloudFront | Lambda@Edge via Terraform/CLI | DynamoDB or S3 | Yes | | Cloudflare | Workers via Wrangler | Workers KV | Yes | | Fastly | Compute@Edge via CLI | KV Store | Yes | -| Akamai | EdgeWorkers via CLI | EdgeKV | Yes | +| Akamai | Not supported | EdgeKV | No | | Vercel | Edge Functions via CLI | KV | Yes | | Generic | Reverse proxy config | External KV | Partial | @@ -145,7 +145,7 @@ Content published in WordPress For CDNs with limited compute capabilities, the edge function can be reduced to a minimal feature set: -1. **Signed URL verification**: Check signed URL query parameters -- valid? pass through : return 403 +1. **Signed URL verification**: verify the Ed25519 signature over the canonical URL -- valid? pass through : return 403. A CloudFront distribution verifies its RSA signed URLs natively, so there the function skips this step. 2. **Bot detection**: Check `User-Agent` for known AI crawler strings -- match? return 403 with `X-Content-Rules` header 3. **Serve `ramp.json`**: Return `/.well-known/ramp.json` inline or proxied from the Exchange @@ -157,7 +157,7 @@ Request arrives at edge: 1. Path = /.well-known/ramp.json? YES -> Return inline ramp.json (or proxy from Exchange) -2. Has signed URL params (Signature, Expires)? +2. Has signed URL params (sig, exp)? YES -> Verify signature Valid + not expired? -> Pass through to origin Invalid or expired? -> 403 Forbidden @@ -168,4 +168,4 @@ Request arrives at edge: 4. Default -> Pass through to origin (non-bot, non-protected) ``` -This minimum viable feature set works on all major CDN platforms including CloudFront Functions (free tier), Cloudflare Workers, Fastly VCL, and Akamai Property Manager rules. +Step 1 needs a runtime that can run code: Cloudflare Workers and Fastly Compute verify the signature themselves, and a CloudFront distribution verifies its own RSA signed URLs without any function at all. A declarative rule language -- Fastly VCL, Akamai Property Manager -- cannot verify a signature, so it can serve `ramp.json` and refuse known crawlers but cannot admit a paid request. diff --git a/website/src/content/docs/components/edge-function/cdn-adapters.mdx b/website/src/content/docs/components/edge-function/cdn-adapters.mdx index c5a173e9..da5bd519 100644 --- a/website/src/content/docs/components/edge-function/cdn-adapters.mdx +++ b/website/src/content/docs/components/edge-function/cdn-adapters.mdx @@ -1,6 +1,6 @@ --- title: "CDN Adapters" -description: "Platform-specific adapters for Cloudflare Workers, CloudFront, Akamai EdgeWorkers, Fastly Compute, and the generic adapter interface" +description: "Platform-specific adapters for Cloudflare Workers, CloudFront, Fastly Compute, and the generic adapter interface" --- The Edge Function is defined as a platform-agnostic interface. Each CDN platform gets a thin adapter that maps platform primitives to the interface. The core decision logic is CDN-agnostic -- every adapter calls the same core function. From 35dbcce2621d1d40e59f1d3b8ee06753b4e105e2 Mon Sep 17 00:00:00 2001 From: noxlesh Date: Wed, 2 Sep 2026 12:45:35 +0300 Subject: [PATCH 09/16] docs(proto): say who holds the delivery-URL private key, and who fetches The signing_key comment said the field carries a public key while the onboarding page pushed ramp-cloudfront-private.pem through it and every other page put the private key inside the Exchange. Both readings cannot be right: either private material crossed the wire labelled public, or the field was dead. The comment now states the custody model the KeyStore implements -- the Exchange signs with a private key it holds and never publishes, the delivery endpoint verifies with the public half. Where the Exchange has to sign with a key the provider generated, as a CloudFront trusted key group requires, the private half is provisioned out of band and never travels in this field. The onboarding page pushes the public half and says where the private half goes. cdn_type said it "mirrors the Exchange-side tenant signing scheme" without saying which value mirrors which. Both mappings are now written down. "The agent is the fetcher" was true for only one of the two deployments. An agent embedding the SDK holds its own key and fetches for itself; a custodial agent never fetches at all, because its key lives in its identity service, which fetches on its behalf. Both present the same key to the edge, which is what makes the binding check work in either case, and neither was represented. "Fully offline / no JWKS fetch required" is true of the fetcher's key, which arrives in the request. The edge still resolves kid to the Exchange's public key from a cached directory, which the same page documents, so the claim now names the key it applies to. The Akamai entry in the walkthrough's adapter link list goes with the target. --- gen/descriptor.binpb | Bin 610720 -> 611597 bytes gen/go/ramp/v1/ramp.pb.go | 14 ++++++-- gen/python/wire/models.py | 4 +-- gen/ts/wire/schemas.ts | 2 +- proto/ramp/v1/ramp.proto | 31 +++++++++++++----- .../docs/getting-started/poc-walkthrough.mdx | 6 ++-- .../getting-started/publisher-onboarding.mdx | 11 +++++-- .../content/docs/protocol/authentication.mdx | 4 +-- 8 files changed, 50 insertions(+), 22 deletions(-) diff --git a/gen/descriptor.binpb b/gen/descriptor.binpb index e9f678fdc88e80d47988fb986ae83039161a2be9..04133ad4cb30fe1e0eb837df1d0aeffee608158f 100644 GIT binary patch delta 43142 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 delta 42359 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 diff --git a/gen/go/ramp/v1/ramp.pb.go b/gen/go/ramp/v1/ramp.pb.go index d85ad0e1..3d35adad 100644 --- a/gen/go/ramp/v1/ramp.pb.go +++ b/gen/go/ramp/v1/ramp.pb.go @@ -8315,8 +8315,15 @@ type DomainVerificationConfirmation struct { Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"` // The challenge token (echoed from DomainVerificationChallenge). Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` - // Optional: signing key to register upon successful verification. - // If present, the key is registered atomically with verification. + // Optional: the delivery endpoint's verification key, registered atomically + // with the domain on successful verification. PUBLIC key material only. + // The Exchange signs delivery URLs with a private key it holds and never + // publishes; a delivery endpoint verifies with the public half and holds + // nothing secret. Where the Exchange has to sign with a key the provider + // generated -- a CloudFront trusted key group is the provider's own AWS + // resource -- the private half is provisioned to the Exchange out of band + // and never travels in this field. + // // Format follows cdn_type: a PEM-encoded RSA public key for "cloudfront", or // the base64url-encoded raw Ed25519 public key (the JWK "x" value) for // "edge-ed25519". @@ -8324,7 +8331,8 @@ type DomainVerificationConfirmation struct { // Which delivery-URL verification scheme this key is for: "edge-ed25519" (a // code-capable edge that verifies the Ed25519 URL signature itself) or // "cloudfront" (AWS CloudFront trusted key groups, RSA, verified natively by - // the CDN). Mirrors the Exchange-side tenant signing scheme. + // the CDN). One value per Exchange-side tenant signing scheme: + // "edge-ed25519" is ED25519, "cloudfront" is AWS_CLOUDFRONT_RSA. CdnType *string `protobuf:"bytes,5,opt,name=cdn_type,json=cdnType,proto3,oneof" json:"cdn_type,omitempty"` // REQUIRED. Bare host of the recipient this request is addressed to (e.g. // "exchange.example" or "exchange.example:8081"). See "Request recipient" in diff --git a/gen/python/wire/models.py b/gen/python/wire/models.py index 9aa84513..47e460d1 100644 --- a/gen/python/wire/models.py +++ b/gen/python/wire/models.py @@ -296,7 +296,7 @@ class DomainVerificationChallenge(WireModel): class DomainVerificationConfirmation(WireModel): cdn_type: str | None = Field( None, - description='Which delivery-URL verification scheme this key is for: "edge-ed25519" (a\n code-capable edge that verifies the Ed25519 URL signature itself) or\n "cloudfront" (AWS CloudFront trusted key groups, RSA, verified natively by\n the CDN). Mirrors the Exchange-side tenant signing scheme.', + description='Which delivery-URL verification scheme this key is for: "edge-ed25519" (a\n code-capable edge that verifies the Ed25519 URL signature itself) or\n "cloudfront" (AWS CloudFront trusted key groups, RSA, verified natively by\n the CDN). One value per Exchange-side tenant signing scheme:\n "edge-ed25519" is ED25519, "cloudfront" is AWS_CLOUDFRONT_RSA.', ) domain: str | None = Field('', description='The domain being verified.') exchange: constr( @@ -313,7 +313,7 @@ class DomainVerificationConfirmation(WireModel): ) signing_key: str | None = Field( None, - description='Optional: signing key to register upon successful verification.\n If present, the key is registered atomically with verification.\n Format follows cdn_type: a PEM-encoded RSA public key for "cloudfront", or\n the base64url-encoded raw Ed25519 public key (the JWK "x" value) for\n "edge-ed25519".', + description='Optional: the delivery endpoint\'s verification key, registered atomically\n with the domain on successful verification. PUBLIC key material only.\n The Exchange signs delivery URLs with a private key it holds and never\n publishes; a delivery endpoint verifies with the public half and holds\n nothing secret. Where the Exchange has to sign with a key the provider\n generated -- a CloudFront trusted key group is the provider\'s own AWS\n resource -- the private half is provisioned to the Exchange out of band\n and never travels in this field.\n\nFormat follows cdn_type: a PEM-encoded RSA public key for "cloudfront", or\n the base64url-encoded raw Ed25519 public key (the JWK "x" value) for\n "edge-ed25519".', ) token: str | None = Field( '', description='The challenge token (echoed from DomainVerificationChallenge).' diff --git a/gen/ts/wire/schemas.ts b/gen/ts/wire/schemas.ts index 10770433..5d4aafb5 100644 --- a/gen/ts/wire/schemas.ts +++ b/gen/ts/wire/schemas.ts @@ -58,7 +58,7 @@ export const DisputeStatusSchema = wire(z.enum(["DISPUTE_STATUS_FILED","DISPUTE_ export const DomainVerificationChallengeSchema = wire(z.object({ "expires_at": z.string().datetime({ offset: true }).describe("When this challenge expires. Provider must confirm before this time.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "token": z.string().describe("Opaque challenge token. Provider must serve this at:\n https://{domain}/.well-known/ramp-verify/{token}").default(""), "ver": z.string().describe("RAMP protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default(""), "verification_url": z.string().describe("The exact URL the Exchange will fetch to verify.").default("") }).describe("DomainVerificationChallenge — Exchange returns a challenge.")); -export const DomainVerificationConfirmationSchema = wire(z.object({ "cdn_type": z.string().describe("Which delivery-URL verification scheme this key is for: \"edge-ed25519\" (a\n code-capable edge that verifies the Ed25519 URL signature itself) or\n \"cloudfront\" (AWS CloudFront trusted key groups, RSA, verified natively by\n the CDN). Mirrors the Exchange-side tenant signing scheme.").optional(), "domain": z.string().describe("The domain being verified.").default(""), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the recipient this request is addressed to (e.g.\n \"exchange.example\" or \"exchange.example:8081\"). See \"Request recipient\" in\n the file header. Distinct from `domain` above, which is the provider domain\n being verified — the subject of the request, not its recipient."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "signing_key": z.string().describe("Optional: signing key to register upon successful verification.\n If present, the key is registered atomically with verification.\n Format follows cdn_type: a PEM-encoded RSA public key for \"cloudfront\", or\n the base64url-encoded raw Ed25519 public key (the JWK \"x\" value) for\n \"edge-ed25519\".").optional(), "token": z.string().describe("The challenge token (echoed from DomainVerificationChallenge).").default(""), "ver": z.string().describe("RAMP protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("DomainVerificationConfirmation — Provider confirms the challenge is placed.")); +export const DomainVerificationConfirmationSchema = wire(z.object({ "cdn_type": z.string().describe("Which delivery-URL verification scheme this key is for: \"edge-ed25519\" (a\n code-capable edge that verifies the Ed25519 URL signature itself) or\n \"cloudfront\" (AWS CloudFront trusted key groups, RSA, verified natively by\n the CDN). One value per Exchange-side tenant signing scheme:\n \"edge-ed25519\" is ED25519, \"cloudfront\" is AWS_CLOUDFRONT_RSA.").optional(), "domain": z.string().describe("The domain being verified.").default(""), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the recipient this request is addressed to (e.g.\n \"exchange.example\" or \"exchange.example:8081\"). See \"Request recipient\" in\n the file header. Distinct from `domain` above, which is the provider domain\n being verified — the subject of the request, not its recipient."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "signing_key": z.string().describe("Optional: the delivery endpoint's verification key, registered atomically\n with the domain on successful verification. PUBLIC key material only.\n The Exchange signs delivery URLs with a private key it holds and never\n publishes; a delivery endpoint verifies with the public half and holds\n nothing secret. Where the Exchange has to sign with a key the provider\n generated -- a CloudFront trusted key group is the provider's own AWS\n resource -- the private half is provisioned to the Exchange out of band\n and never travels in this field.\n\nFormat follows cdn_type: a PEM-encoded RSA public key for \"cloudfront\", or\n the base64url-encoded raw Ed25519 public key (the JWK \"x\" value) for\n \"edge-ed25519\".").optional(), "token": z.string().describe("The challenge token (echoed from DomainVerificationChallenge).").default(""), "ver": z.string().describe("RAMP protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("DomainVerificationConfirmation — Provider confirms the challenge is placed.")); export const DomainVerificationFailureSchema = wire(z.object({ "reason": z.enum(["DOMAIN_VERIFICATION_FAILURE_REASON_CHALLENGE_NOT_FOUND","DOMAIN_VERIFICATION_FAILURE_REASON_CHALLENGE_MISMATCH","DOMAIN_VERIFICATION_FAILURE_REASON_CHALLENGE_EXPIRED","DOMAIN_VERIFICATION_FAILURE_REASON_FETCH_FAILED","DOMAIN_VERIFICATION_FAILURE_REASON_EXCHANGE_NOT_AUTHORIZED","DOMAIN_VERIFICATION_FAILURE_REASON_KEY_REGISTRATION_FAILED"]).describe("The failure reason (defined-only, non-zero)") }).describe("DomainVerificationFailure — RequestDomainVerification / ConfirmDomainVerification failed.")); diff --git a/proto/ramp/v1/ramp.proto b/proto/ramp/v1/ramp.proto index 7451200f..9c935eb9 100644 --- a/proto/ramp/v1/ramp.proto +++ b/proto/ramp/v1/ramp.proto @@ -75,12 +75,17 @@ import "ramp/v1/vocab.proto"; // by embedding agent_identity_hash — the RFC 7638 JWK Thumbprint (SHA-256) // of the agent's Ed25519 request-signing key — as the URL's agent_id query // parameter, which the URL signature covers, and echoing it in the response. -// A capable delivery endpoint (edge function) verifies the binding fully -// offline: verify the URL's Ed25519 signature against the Exchange's -// published public key (proves the hash is Exchange-issued and untampered), -// then require the fetcher to present its public key and an RFC 9421 -// signature over the retrieval request, and check -// thumbprint(presented key) == agent_identity_hash. No JWKS fetch required. +// A capable delivery endpoint (edge function) verifies the binding without +// contacting the fetcher's directory: verify the URL's Ed25519 signature +// against the Exchange's published public key (proves the hash is +// Exchange-issued and untampered), then require the fetcher to present its +// public key and an RFC 9421 signature over the retrieval request, and check +// thumbprint(presented key) == agent_identity_hash. The fetcher's key arrives +// in the request, so no JWKS fetch is needed for it; the edge still resolves +// kid to the Exchange's public key, from a cached directory or pinned config. +// The fetcher is whoever holds the agent's request-signing key: an agent +// embedding the SDK fetches for itself, while a custodial agent does not +// fetch at all and its identity service fetches on its behalf. // Enforcement is NOT mandatory: a bearer-only signed-URL CDN that cannot run // code falls back to the URL signature + short TTL + TLS. RAMP reference // implementations run on edge functions and DO enforce it. @@ -3666,8 +3671,15 @@ message DomainVerificationConfirmation { // The challenge token (echoed from DomainVerificationChallenge). string token = 3; - // Optional: signing key to register upon successful verification. - // If present, the key is registered atomically with verification. + // Optional: the delivery endpoint's verification key, registered atomically + // with the domain on successful verification. PUBLIC key material only. + // The Exchange signs delivery URLs with a private key it holds and never + // publishes; a delivery endpoint verifies with the public half and holds + // nothing secret. Where the Exchange has to sign with a key the provider + // generated -- a CloudFront trusted key group is the provider's own AWS + // resource -- the private half is provisioned to the Exchange out of band + // and never travels in this field. + // // Format follows cdn_type: a PEM-encoded RSA public key for "cloudfront", or // the base64url-encoded raw Ed25519 public key (the JWK "x" value) for // "edge-ed25519". @@ -3676,7 +3688,8 @@ message DomainVerificationConfirmation { // Which delivery-URL verification scheme this key is for: "edge-ed25519" (a // code-capable edge that verifies the Ed25519 URL signature itself) or // "cloudfront" (AWS CloudFront trusted key groups, RSA, verified natively by - // the CDN). Mirrors the Exchange-side tenant signing scheme. + // the CDN). One value per Exchange-side tenant signing scheme: + // "edge-ed25519" is ED25519, "cloudfront" is AWS_CLOUDFRONT_RSA. optional string cdn_type = 5; // REQUIRED. Bare host of the recipient this request is addressed to (e.g. diff --git a/website/src/content/docs/getting-started/poc-walkthrough.mdx b/website/src/content/docs/getting-started/poc-walkthrough.mdx index d022bd91..f3ead1e5 100644 --- a/website/src/content/docs/getting-started/poc-walkthrough.mdx +++ b/website/src/content/docs/getting-started/poc-walkthrough.mdx @@ -187,7 +187,7 @@ Response: { "transaction_id": "txn-1773682700352684307", "billing_id": "bill-000002", - "retrieval_endpoint": "https://cdn.ramp-protocol.org/premium/article.html?agent_id=NzbLsXh8...C9Xs&exp=1773683000&kid=vJ3xR1mQ...&sig=3ab495e3...48823a14", + "retrieval_endpoint": "https://cdn.ramp-protocol.org/premium/article.html?agent_id=NzbLsXh8...C9Xs&exp=1773683000&kid=vJ3xR1mQ...&sig=k7Qm2xR9vT4nB8aL...", "cost": { "amount": "0.05", "currency": "USD" }, "delivery_method": "DELIVERY_METHOD_INSTRUCTIONS", "reporting_obligation": { @@ -404,7 +404,7 @@ For production deployment, see: - [Exchange configuration](/components/exchange/configuration) — environment variables, secrets, tenant setup - [Multi-tenant architecture](/components/exchange/multi-tenant) — how one Exchange serves multiple providers - [Scaling](/components/exchange/scaling) — from single instance to 10K+ RPS -- [Edge function deployment](/components/edge-function/deployment) — CDN-specific adapters for [CloudFront, Cloudflare, Akamai, Fastly](/components/edge-function/cdn-adapters) +- [Edge function deployment](/components/edge-function/deployment) — CDN-specific adapters for [CloudFront, Cloudflare, Fastly](/components/edge-function/cdn-adapters) --- @@ -416,7 +416,7 @@ For production deployment, see: | 1 | [`DiscoverResources`](/components/exchange/request-flows) | Agent queries Exchange for offers on URIs | [Request Flows](/components/exchange/request-flows) | | 2 | — | Agent/[Broker](/components/broker/overview) selects best offer | [Selection Engine](/components/broker/selection-engine) | | 3 | [`ExecuteTransaction`](/components/exchange/request-flows) | Agent commits to offer, receives [signed URL](/components/edge-function/signed-url-verification) | [Billing Adapter](/components/exchange/billing-adapter) | -| 4 | — | Agent fetches resource via signed URL ([edge function](/components/edge-function/overview) verifies) | [Bot Detection](/components/edge-function/bot-detection) | +| 4 | — | The key holder fetches the resource via the signed URL — the agent itself with the SDK, or its identity service when the key is custodial ([edge function](/components/edge-function/overview) verifies) | [Bot Detection](/components/edge-function/bot-detection) | | 5 | [`ReportUsage`](/components/exchange/request-flows) | Agent reports consumed quantity (mandatory) | [Budget & Reporting](/components/agent-sdk/budget-reporting) | **Security at every boundary** ([full threat model](/security/threat-model)): diff --git a/website/src/content/docs/getting-started/publisher-onboarding.mdx b/website/src/content/docs/getting-started/publisher-onboarding.mdx index 707d754b..76196b22 100644 --- a/website/src/content/docs/getting-started/publisher-onboarding.mdx +++ b/website/src/content/docs/getting-started/publisher-onboarding.mdx @@ -21,7 +21,7 @@ The provider grants CDN access during onboarding. The Exchange generates keys, c **Best for**: Single-exchange providers who want zero operational overhead. -**What you provide**: IAM role (CloudFront), API credentials (Akamai/Fastly). +**What you provide**: IAM role (CloudFront), API credentials (Fastly). ### Model B: Provider-Managed (Opt-In) @@ -85,9 +85,16 @@ ramp-cli key push \ --exchange mp.ssp.com \ --domain techcrunch.com \ --cdn cloudfront \ - --key-file ./ramp-cloudfront-private.pem + --public-key ./ramp-cloudfront-public.pem ``` +The push carries the public half only -- it is what +[`DomainVerificationConfirmation.signing_key`](/reference/proto-ramp/#domainverificationconfirmation) +registers. The Exchange signs delivery URLs, so under Model B it also needs the +private half; that is delivered to the Exchange operator through their key +provisioning channel, never through this RPC. Under Model A the Exchange +generates both halves itself and the question does not arise. + Output: ``` Requesting domain verification for techcrunch.com... diff --git a/website/src/content/docs/protocol/authentication.mdx b/website/src/content/docs/protocol/authentication.mdx index 32cfc16d..db4b38c9 100644 --- a/website/src/content/docs/protocol/authentication.mdx +++ b/website/src/content/docs/protocol/authentication.mdx @@ -380,8 +380,8 @@ RFC 9421 is the **only** request-authentication mechanism. A request without a v A signed `retrieval_endpoint` returned by a transaction is, by default, a bearer token: anyone holding it before `expires_at` can fetch. RAMP **optionally** binds the URL to the purchasing agent, DPoP-style ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)): - The Exchange embeds `agent_identity_hash` — the [RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) JWK Thumbprint (SHA-256) of the agent's Ed25519 request-signing key — as the URL's `agent_id` query parameter, which the URL signature covers, and echoes it on the execute-path response (`TransactionResponse.agent_identity_hash`, produced directly by the Exchange — the Broker is not in the execute response path for this field). -- A capable delivery endpoint (edge function) verifies the binding **fully offline**: verify the URL's Ed25519 signature against the Exchange's published public key (proves the hash is Exchange-issued and untampered), then require the fetcher to present its public key and an RFC 9421 signature over the retrieval request, and check `thumbprint(presented key) == agent_identity_hash`. No JWKS fetch required. -- Bound to the agent's request-signing key, never to the principal/delegation — the agent is the fetcher, and is exactly one key per transaction. +- A capable delivery endpoint (edge function) verifies the binding without contacting the fetcher's directory: verify the URL's Ed25519 signature against the Exchange's published public key (proves the hash is Exchange-issued and untampered), then require the fetcher to present its public key and an RFC 9421 signature over the retrieval request, and check `thumbprint(presented key) == agent_identity_hash`. The fetcher's key arrives in the request, so no JWKS fetch is needed for it. The edge does resolve `kid` to the Exchange's public key, from a cached directory or from keys pinned in its configuration. +- Bound to the request-signing key of whoever fetches, never to the principal/delegation, and exactly one key per transaction. Who that is depends on where the key lives: an agent embedding the SDK holds its own key and fetches for itself, while a custodial agent never fetches at all -- its identity service holds the key and fetches on its behalf. Either way the fetcher is the key holder, which is what the edge checks. - Enforcement is **NOT mandatory**: a CDN that cannot run code uses its native signed-URL scheme (a CloudFront RSA canned policy, for example) + short TTL + TLS. RAMP reference implementations run on edge functions and **do** enforce it. See [Signed URL Verification](/components/edge-function/signed-url-verification) for the full edge-function verification flow and the stolen-URL threat analysis. From 2861e09c7840384a992e9399f0d2166ce623f62f Mon Sep 17 00:00:00 2001 From: noxlesh Date: Wed, 2 Sep 2026 12:45:45 +0300 Subject: [PATCH 10/16] docs(protocol): make the worked examples pass their own checks The scenario walkthrough mints the agent identity hash at step 7 as SHA256(requester.id + ":" + requester.domain), puts it in the URL's agent_id, and then verifies it at the edge as thumbprint(presented public key). The example cannot pass: a hash of two public identifiers is not the thumbprint of a key, and anyone can recompute it, so it binds nothing. Step 7 now computes the RFC 7638 thumbprint of the key whose RFC 9421 signature the Exchange just verified, and says why the identifier hash was not a binding. The placeholder values told the same story backwards. agent_identity_hash is a thumbprint, which is base64url, and the examples printed hex; sig is base64url with no padding, and three walkthroughs printed hex there too. A reader copying the shape of these values would build the wrong parser. The EU walkthrough's publisher parameter is uri=CELEX:32024R1689. The signer percent-encodes the query, so the byte the signature covers is CELEX%3A..., and the unescaped colon in the example is a signature that would not verify. --- .../docs/protocol/scenario-walkthrough.mdx | 21 ++++++++++++------- .../docs/protocol/transaction-flow.mdx | 6 +++--- .../protocol/walkthrough-credit-report.mdx | 4 ++-- .../protocol/walkthrough-eu-regulation.mdx | 4 ++-- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/website/src/content/docs/protocol/scenario-walkthrough.mdx b/website/src/content/docs/protocol/scenario-walkthrough.mdx index 98dca367..1abde2d3 100644 --- a/website/src/content/docs/protocol/scenario-walkthrough.mdx +++ b/website/src/content/docs/protocol/scenario-walkthrough.mdx @@ -743,7 +743,12 @@ The request body is identical to the direct case; the forwarding chain lives in - 850000 > 3300 -> sufficient quota, proceed - Deduct 3300 from quota -> new remaining: 846700 7. Compute agent identity hash: - agentHash = SHA256(requester.id + ":" + requester.domain) = SHA256("claude-agent-001:claude.ai") = "e3b0c442..." + agentHash = RFC 7638 JWK thumbprint of the agent's request-signing public key + = the keyid this request's RFC 9421 signature was just verified with + = "NzbLsXh8..." + (A hash of the agent's id and domain would bind nothing: the edge checks the + hash against a key the fetcher must prove it holds, and anyone can recompute + a hash of two public identifiers.) 8. Write transaction log (WAL) — MUST succeed before signing URL: { transaction_id: "txn-alpha-001", @@ -751,7 +756,7 @@ The request body is identical to the direct case; the forwarding chain lives in offer_id: "4f155204-a53e-42aa-8471-154f87d7d3d5", subscription_id: "SUB-ANTHROPIC-HEARST-2026", amount: "0", - agent_identity_hash: "e3b0c442...", + agent_identity_hash: "NzbLsXh8...", offer_snapshot_json: "", reporting_required: true, reporting_deadline: "2026-03-16T15:00:00Z" @@ -763,7 +768,7 @@ The request body is identical to the direct case; the forwarding chain lives in Signed URL: https://cdn.techcrunch.com/server/premium/ai-regulation-2026.html - ?agent_id=e3b0c442... + ?agent_id=NzbLsXh8... &exp=1710517800 &kid=vJ3xR1mQ... &sig=k7Qm2xR9vT4nB8aL... @@ -776,13 +781,13 @@ The request body is identical to the direct case; the forwarding chain lives in ```json { "ver": "1.0", - "agent_identity_hash": "e3b0c442...", + "agent_identity_hash": "NzbLsXh8...", "items": [ { "transaction_id": "txn-alpha-001", "billing_id": "bill-sub-alpha-001", "resource_title": "AI Regulation: What Providers Need to Know", - "retrieval_endpoint": "https://cdn.techcrunch.com/server/premium/ai-regulation-2026.html?agent_id=e3b0c442...&exp=1710517800&kid=vJ3xR1mQ...&sig=k7Qm2xR9vT4nB8aL...", + "retrieval_endpoint": "https://cdn.techcrunch.com/server/premium/ai-regulation-2026.html?agent_id=NzbLsXh8...&exp=1710517800&kid=vJ3xR1mQ...&sig=k7Qm2xR9vT4nB8aL...", "cost": { "amount": "0", "currency": "USD", "unit_cost": "0" }, "delivery_method": "DELIVERY_METHOD_INSTRUCTIONS", "subscription_id": "SUB-ANTHROPIC-HEARST-2026", @@ -806,7 +811,7 @@ The `subscription_unit_value` field carries the per-unit cost ($0.05) even thoug ``` Agent -> CDN: GET https://cdn.techcrunch.com/server/premium/ai-regulation-2026.html - ?agent_id=e3b0c442... + ?agent_id=NzbLsXh8... &exp=1710517800 &kid=vJ3xR1mQ... &sig=k7Qm2xR9vT4nB8aL... @@ -901,7 +906,7 @@ Three independent records that must agree: ``` 1. Edge delivery log (Hearst controls): - url_hash=9f2c7a4e... fetched at 15:25:00, 200 OK, 45KB - - agent_id=e3b0c442..., signature and agent binding verified by the edge + - agent_id=NzbLsXh8..., signature and agent binding verified by the edge 2. Exchange transaction log (SSP-Alpha controls): - txn-alpha-001: signed_url_hash=9f2c7a4e... (the join key to the log above) @@ -955,7 +960,7 @@ Response includes signed Offer snapshots for every transaction: }, "cost": { "amount": "0", "currency": "USD" }, "subscription_unit_value": { "amount": "0.05", "currency": "USD" }, - "agent_id": "e3b0c442...", + "agent_id": "NzbLsXh8...", "timestamp": "2026-03-15T15:25:00Z" } ] diff --git a/website/src/content/docs/protocol/transaction-flow.mdx b/website/src/content/docs/protocol/transaction-flow.mdx index f2b4ad9d..15bf0af5 100644 --- a/website/src/content/docs/protocol/transaction-flow.mdx +++ b/website/src/content/docs/protocol/transaction-flow.mdx @@ -338,7 +338,7 @@ Each item's `offer` is the full signed Offer reflected back exactly as received "expires_at": "2026-03-14T02:30:00Z" } ], - "agent_identity_hash": "e3b0c44298fc1c14..." + "agent_identity_hash": "NzbLsXh8..." } ``` @@ -419,7 +419,7 @@ For batch transactions, use the `items` array: } ], "total_cost": { "amount": "0.10", "currency": "USD" }, - "agent_identity_hash": "e3b0c44298fc1c14..." + "agent_identity_hash": "NzbLsXh8..." } ``` @@ -441,7 +441,7 @@ Each item gets its own `transaction_id`, `billing_id`, and signed URL. Batch tra "reporting_obligation": { "required": true, "window": "86400s", "required_fields": ["transaction_id", "function", "consumed_quantity"] } } ], - "agent_identity_hash": "e3b0c44298fc1c14..." + "agent_identity_hash": "NzbLsXh8..." } ``` diff --git a/website/src/content/docs/protocol/walkthrough-credit-report.mdx b/website/src/content/docs/protocol/walkthrough-credit-report.mdx index c8bf156b..5fcd412b 100644 --- a/website/src/content/docs/protocol/walkthrough-credit-report.mdx +++ b/website/src/content/docs/protocol/walkthrough-credit-report.mdx @@ -463,7 +463,7 @@ Response: { "transaction_id": "txn-dnb-acme-001", "billing_id": "bill-dnb-acme-001", - "retrieval_endpoint": "https://api.dnb.com/ramp/deliver/123456789/standard?agent_id=9c5f0d3e...&exp=1742475900&kid=hT4nR8yK...&sig=b8c9d0e1...", + "retrieval_endpoint": "https://api.dnb.com/ramp/deliver/123456789/standard?agent_id=9c5f0d3e...&exp=1742475900&kid=hT4nR8yK...&sig=pQ7wF2sN9xC5tH1e...", "cost": { "amount": "61.99", "currency": "USD" @@ -489,7 +489,7 @@ The signed URL is valid for 5 minutes. The `agent_identity_hash` (the agent's RF The agent fetches the credit report via the signed URL: ```bash -GET https://api.dnb.com/ramp/deliver/123456789/standard?agent_id=9c5f0d3e...&exp=1742475900&kid=hT4nR8yK...&sig=b8c9d0e1... +GET https://api.dnb.com/ramp/deliver/123456789/standard?agent_id=9c5f0d3e...&exp=1742475900&kid=hT4nR8yK...&sig=pQ7wF2sN9xC5tH1e... ``` D&B's delivery endpoint verifies: diff --git a/website/src/content/docs/protocol/walkthrough-eu-regulation.mdx b/website/src/content/docs/protocol/walkthrough-eu-regulation.mdx index dff759af..fb83e436 100644 --- a/website/src/content/docs/protocol/walkthrough-eu-regulation.mdx +++ b/website/src/content/docs/protocol/walkthrough-eu-regulation.mdx @@ -391,7 +391,7 @@ Response: { "transaction_id": "txn-eurlex-001", "billing_id": "bill-eurlex-001", - "retrieval_endpoint": "https://cdn.eurlex.europa.eu/legal-content/EN/TXT/XML/?agent_id=3e8f1a2b...&exp=1742404200&kid=gY2kP7mV...&sig=e7f8a9...&uri=CELEX:32024R1689", + "retrieval_endpoint": "https://cdn.eurlex.europa.eu/legal-content/EN/TXT/XML/?agent_id=3e8f1a2b...&exp=1742404200&kid=gY2kP7mV...&sig=rM4vK8bT2yD6qJ0z...&uri=CELEX%3A32024R1689", "cost": { "amount": "0", "currency": "EUR" @@ -421,7 +421,7 @@ The agent fetches via the signed URL: GET https://cdn.eurlex.europa.eu/legal-content/EN/TXT/XML/ ?agent_id=3e8f1a2b...&exp=1742404200&kid=gY2kP7mV... &sig=e7f8a9... - &uri=CELEX:32024R1689 + &uri=CELEX%3A32024R1689 ``` EUR-Lex delivers the regulation as **Akoma Ntoso XML** -- the OASIS standard for legislative documents. Because `resource_mutability` is `STATIC`, the agent verifies the SHA-256 hash against `identity.content_hash`. A match confirms this is the exact Official Journal text as published on 12 July 2024. From b9887af3c7b412a791e25b37423104b23bf89397 Mon Sep 17 00:00:00 2001 From: noxlesh Date: Wed, 2 Sep 2026 12:45:56 +0300 Subject: [PATCH 11/16] docs(ci): fail the run on a denylist pattern that cannot compile '?txn_id=' is not a valid ERE. BSD grep -- the one on the macOS runner -- rejects a leading '?' with "repetition-operator operand invalid" and exits 2, which the loop's 2>/dev/null hid and its || true swallowed. The pattern was dead there and the gate reported clean. GNU grep accepts the same pattern, so a Linux-only check never noticed. The pattern is now [?]txn_id=, and every pattern in both lists is compiled against empty input before use: exit 0 or 1 means it compiled, 2 or more fails the run with the pattern named. Verified by planting an uncompilable pattern and watching the script exit 1, and by planting each of the six retired phrasings in a page and watching the gate catch it. The shared-secret pattern only allowed three characters between Exchange and CDN, so "Exchange <-> CDN shared secret" passed; it now allows eight. Four retired phrasings the sweep deleted but the list never held out are added: "hex for HMAC", "HMAC verification", signingMode, and "hmac" as a cdn_type value. The bare token HMAC stays legal on purpose, because a page may name HMAC as the alternative this scheme rejects, and the changelog does exactly that. The justification comment cited report tokens, which live in another repository and cannot be checked from here; it now cites the changelog. --- scripts/check-doc-conformance.sh | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/scripts/check-doc-conformance.sh b/scripts/check-doc-conformance.sh index 693eaccd..9a88aad7 100755 --- a/scripts/check-doc-conformance.sh +++ b/scripts/check-doc-conformance.sh @@ -107,17 +107,19 @@ patterns=( # PUBLIC key; the CloudFront-native path uses RSA, verified by the CDN. No # implementation ever used HMAC and no secret ever reaches a delivery endpoint. # These are the retired phrasings of that claim. The bare token "HMAC" stays - # legal: report tokens are genuinely HMAC-hashed, and a doc may cite HMAC as a - # rejected alternative. + # legal on purpose: a page may name HMAC as the alternative this scheme + # rejects, and the changelog entry above does exactly that. Only the phrasings + # that assert HMAC IS the scheme are denied. 'HMAC-SHA256 signed URL' 'HMAC signed URL' 'HMAC-signed URL' 'URL HMAC' - 'HMAC canonicalization' 'HMAC [Cc]anonicalization' 'hmac-sha256-' + 'HMAC [Cc]anonicalization' 'hmac-sha256-' 'hex for HMAC' 'HMAC verification' 'HMAC secret' 'hmacSecret' 'HMAC_SECRET' 'CDNGenericHMAC' 'Generic HMAC' - 'Exchange.CDN shared secret' 'shared secret between Exchange and CDN' + 'Exchange.{1,8}CDN shared secret' 'shared secret between Exchange and CDN' + 'signingMode' '"hmac"' # The signed URL carries no transaction-id parameter and no `expires`; the # parameters are agent_id, exp, kid and sig, and reconciliation joins on # signed_url_hash. Anchored to the URL forms so the patterns do NOT collide # with the legitimate slog field name `txn_id` in Go samples. - '&txn_id=' '?txn_id=' 'txn_id=txn-' 'baseURL.nexpires' + '&txn_id=' '[?]txn_id=' 'txn_id=txn-' 'baseURL.nexpires' ) # Files where naming a removed identifier is legitimate (they record history). @@ -145,8 +147,23 @@ for f in "$proto_ramp" "$event_types" "$auth"; do [ -f "$f" ] || { echo "::error::check-doc-conformance: required file missing (renamed? update this script): $f"; status=1; } done +# A pattern that does not compile can never match, so it reports clean forever. +# `?txn_id=` shipped exactly that way. BSD grep -- the one on the macOS runner -- +# rejects a leading `?` with "repetition-operator operand invalid" and exits 2, +# which `2>/dev/null` hid and `|| true` swallowed, so the pattern was dead there +# and nobody could see it. GNU grep accepts the same pattern, so a Linux-only +# check never notices. Compile every pattern against empty input first: exit 0 or +# 1 means it compiled, 2 or more means it did not and the run fails. +assert_ere() { + printf '' | grep -E -- "$1" >/dev/null 2>&1 + [ "$?" -lt 2 ] && return 0 + echo "::error::check-doc-conformance: pattern is not a valid ERE, so it can never match: $1" + return 1 +} + # --- 1. Denylist: removed/renamed identifiers must not reappear ------------- for p in "${patterns[@]}"; do + assert_ere "$p" || { status=1; continue; } hits=$(grep -rEn -- "$p" "${roots[@]}" 2>/dev/null | grep -Ev "$exclude_re" || true) if [ -n "$hits" ]; then echo "::error::removed/renamed identifier still present: ${p}" @@ -176,6 +193,7 @@ vocab=( ) for entry in "${vocab[@]}"; do p=${entry%%#*}; canon=${entry#*#} + assert_ere "$p" || { status=1; continue; } hits=$(grep -rEn -- "$p" "${roots[@]}" 2>/dev/null | grep -Ev "$exclude_re" || true) if [ -n "$hits" ]; then echo "::error::non-canonical role vocabulary (prose) — use '${canon}' (Requester/Provider are the standard role terms):" From 16afc5055dd2e1bd3c9fd66d90be4d7b946c9feb Mon Sep 17 00:00:00 2001 From: noxlesh Date: Wed, 2 Sep 2026 12:46:05 +0300 Subject: [PATCH 12/16] docs(edge): say what the three verifiers do about check ordering The page claimed the signature is checked before the expiry, as a deliberate property, and the changelog repeated it. Only the Go verifier does that. The TypeScript and Python verifiers read exp first and return "expired" without touching the signature, and the edge runs the TypeScript face. Both orders refuse the same URLs; what differs is the reason a caller sees. The page now says so and marks the refusal reason a diagnostic rather than a contract. Aligning the three verifiers is a change to shipped code and belongs in its own ticket. The key directory is not tenant-scoped: an Exchange publishes one directory for all its tenants and the edge accepts any kid that resolves in it, so a URL signed with another tenant's key verifies on your edge. That matters when one Exchange serves publishers who do not trust each other, and pinning the expected keys is the control. Neither the property nor the control was written down. --- .../edge-function/signed-url-verification.mdx | 11 +++++++++-- website/src/content/docs/reference/changelog.mdx | 8 +++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/website/src/content/docs/components/edge-function/signed-url-verification.mdx b/website/src/content/docs/components/edge-function/signed-url-verification.mdx index 2dac7f21..c665c218 100644 --- a/website/src/content/docs/components/edge-function/signed-url-verification.mdx +++ b/website/src/content/docs/components/edge-function/signed-url-verification.mdx @@ -66,7 +66,7 @@ sequenceDiagram Agent->>Edge: GET /premium/article.html?agent_id=&exp=1773451434&kid=&sig=... - Note over Edge: 1. No sig parameter? bot gate, then origin
2. Resolve kid to a public key
3. Rebuild "GET\n<canonical URL>"
4. Ed25519 verify, then check exp
5. Method gate
6. Proof of possession, if agent_id is present + Note over Edge: 1. No sig parameter? bot gate, then origin
2. Resolve kid to a public key
3. Rebuild "GET\n<canonical URL>"
4. Ed25519 verify and exp check
5. Method gate
6. Proof of possession, if agent_id is present Edge->>Dir: fetch public keys (cached; skipped when keys are pinned in config) Dir-->>Edge: JWK Set @@ -88,7 +88,7 @@ Two behaviours here are deliberate and neither is obvious. **Verification fails closed.** If the key-resolution leg throws — the directory is unreachable, the response is malformed — the edge answers `503 verify_unavailable`. It never falls through to the origin. -**The signature is checked before the expiry.** Both `exp` and `sig` come from the same untrusted URL. Reading `exp` first would mean acting on attacker-supplied bytes before anything had proven they were Exchange-issued. +**The order of the signature and expiry checks differs by language, and the reason a caller sees follows from it.** The Go verifier verifies the signature first, so it reports `expired` only for a URL whose signature was good. The TypeScript and Python verifiers read `exp` first, so an expired URL is reported as `expired` whether or not its signature would have verified. Both orders refuse exactly the same URLs. The refusal reason is a diagnostic, not a wire contract -- do not branch on which one comes back. Refusals from the URL check: @@ -214,6 +214,13 @@ The edge resolves `kid` to a key from the Exchange's key directory at `/.well-kn **Directory keys carry no key identifier of their own.** The edge computes the map key locally as the RFC 7638 thumbprint of the key material. That is why `kid` on the URL is a thumbprint rather than an operator-chosen label: there is nothing a publisher could assert that the edge would have to trust. +**The directory is not tenant-scoped, so pin the keys if you need it to be.** An +Exchange publishes one directory for all its tenants, and the edge accepts any +`kid` that resolves in it. A URL signed with another tenant's key therefore +verifies on your edge, which matters when one Exchange serves publishers who do +not trust each other. Pinning the keys you expect in `verifyKeys` closes it: a +`kid` outside that set never resolves, and the fetch never happens. + ```mermaid graph LR subgraph "Exchange" diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index 10029f0c..58a9604d 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -32,9 +32,11 @@ Ed25519 verifier. Akamai is no longer documented as a supported delivery target, because EdgeAuth verifies with a secret shared with the CDN. The signed-URL verification page is rewritten against the shipped verifier, and -now documents what it always did but never said: verification fails closed, the -signature is checked before the expiry, agent binding is enforced by default, and -the delivery endpoint holds public keys only. +now documents what it always did but never said: verification fails closed, +agent binding is enforced by default, and the delivery endpoint holds public keys +only. It also records that the three SDK verifiers order the signature and expiry +checks differently, so the refusal reason for an expired URL is a diagnostic and +not a contract. **`Offer.offer_id` is documented as an opaque unique identifier, not a resource key (comment clarification; no wire change).** The comment already said the id is From 27aa659c12160bfd1ffbdc0b79543f8be9538a8d Mon Sep 17 00:00:00 2001 From: noxlesh Date: Wed, 2 Sep 2026 12:46:17 +0300 Subject: [PATCH 13/16] docs(edge): finish the pages the sweep only half-corrected The sweep matched retired phrasings rather than reading each touched page from top to bottom, so changed lines ended up contradicting unchanged ones on the same page. Three places still provisioned a secret to a delivery endpoint whose headline claim is that it holds none: a degradation row reading "signing secret missing", and two configuration descriptions listing signing secrets as an environment variable. The edge's failure there is that it cannot resolve a verification key, which answers 503 verify_unavailable. Two config surfaces described the same worker and disagreed. One declared maxUrlTtlSeconds, which no verifier reads -- the only expiry bound is the exp the Exchange signed -- and agentBindingEnabled, while the other had already become enforce_binding, on by default. The overview said binding and single-use were each enforced "if enabled" without saying which way either defaults; binding is on, single-use is off and best-effort. Akamai survived as a delivery target in a deploy sequence, a packaging option, a deploy-command table and an architecture diagram, on pages that also say it is not supported. Its EdgeAuth scheme verifies with a secret shared with the CDN, which is the model this correction removes. --- .../content/docs/architecture/production-architecture.mdx | 6 +++--- .../docs/components/edge-function/cdn-adapters.mdx | 7 ++----- .../content/docs/components/edge-function/deployment.mdx | 8 +++----- .../content/docs/components/edge-function/overview.mdx | 6 +++--- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/website/src/content/docs/architecture/production-architecture.mdx b/website/src/content/docs/architecture/production-architecture.mdx index 76d8478c..9d168be3 100644 --- a/website/src/content/docs/architecture/production-architecture.mdx +++ b/website/src/content/docs/architecture/production-architecture.mdx @@ -14,7 +14,7 @@ Open-source infrastructure components for metered resource access. Like Prebid.j | **Exchange Node** | Exchange operator / Provider / 3rd party | Operator's billing, provider catalogs, resource delivery | | **RAMP Broker** | AI company / 3rd party | Agent's budget system, auth, multi-Exchange selection | | **Agent SDK** | AI agent developers | Embedded in agent process; handles discovery, negotiation, resource fetch, budget, and usage reporting via one-liner `Fetch(url)` API | -| **Edge Function** | Provider | Provider's CDN (CF/Akamai/Fastly) | +| **Edge Function** | Provider | Provider's CDN (CloudFront/Cloudflare/Fastly) | | **Client SDK** | AI agent developers | Low-level generated Connect client for direct Exchange RPC access (no orchestration logic) | ### Role Mapping (Ad-Tech Analogy) @@ -199,8 +199,8 @@ Cross-provider dedup is a **Broker-level concern**, not a protocol concern. The ▼ ┌─────────────────────┐ │ Edge / CDN │ ← Provider's existing CDN - │ (CF/Akamai/Fastly)│ Lightweight edge function - │ │ Returns 403 + X-Content-Rules + │ (Cloudflare, Fastly│ Lightweight edge function + │ CloudFront) │ Returns 403 + X-Content-Rules └──────────┬──────────┘ │ ┌────────────┼────────────┐ diff --git a/website/src/content/docs/components/edge-function/cdn-adapters.mdx b/website/src/content/docs/components/edge-function/cdn-adapters.mdx index da5bd519..d470de7f 100644 --- a/website/src/content/docs/components/edge-function/cdn-adapters.mdx +++ b/website/src/content/docs/components/edge-function/cdn-adapters.mdx @@ -82,14 +82,11 @@ interface EdgeConfig { /** Optional pinned public keys; a covering `kid` skips the directory fetch. */ verifyKeys?: JsonWebKey[]; - /** Maximum signed URL TTL in seconds (enforced independently). */ - maxUrlTtlSeconds: number; - /** Whether to enforce single-use URLs (best-effort). Requires kv. */ singleUseEnabled: boolean; - /** Whether to enforce agent identity binding. */ - agentBindingEnabled: boolean; + /** Whether to enforce agent identity binding. Defaults to true. */ + enforceBinding: boolean; } ``` diff --git a/website/src/content/docs/components/edge-function/deployment.mdx b/website/src/content/docs/components/edge-function/deployment.mdx index c0eb9b80..a3901a29 100644 --- a/website/src/content/docs/components/edge-function/deployment.mdx +++ b/website/src/content/docs/components/edge-function/deployment.mdx @@ -23,7 +23,7 @@ sequenceDiagram RAMP-->>Provider: EdgeConfigDocument + deployment instructions Provider->>CDN: Deploy edge function (platform-specific) - Note over CDN: CloudFront: deploy CF Function + update behavior
Cloudflare: wrangler deploy
Akamai: upload EdgeWorker bundle
Fastly: fastly compute deploy + Note over CDN: CloudFront: deploy CF Function + update behavior
Cloudflare: wrangler deploy
Fastly: fastly compute deploy Provider->>CDN: Verify: GET /.well-known/ramp.json CDN-->>Provider: 200 OK + WellKnownManifest @@ -82,7 +82,6 @@ For providers who prefer click-to-deploy: - **Cloudflare Apps** -- installable from Cloudflare dashboard. Provider enters Exchange endpoint, protected paths. The app deploys a Worker. - **AWS CloudFormation StackSet** -- one-click deploy via AWS Exchange or a shared CloudFormation template. -- **Akamai Exchange** -- EdgeWorker bundle with configuration UI in Akamai Control Center. ### Format 4: CLI Tool (Planned) @@ -116,7 +115,7 @@ When available, the CLI will generate the platform-specific deployment artifact ```mermaid graph TD subgraph "Config Sources" - ENV[Environment Variables
Signing secrets, mode flags] + ENV[Environment Variables
Exchange directory URL,
mode flags] KV[Edge KV Store
Bot patterns, manifest,
content policy] INLINE[Inline / Compiled
Provider domain,
Exchange endpoint] CMS[CMS API
Content policies,
path rules] @@ -137,7 +136,7 @@ graph TD **Merge order** (later overrides earlier): 1. **Inline / compiled defaults** -- provider domain, protocol version. -2. **Environment variables** -- signing secrets, mode flags, Exchange endpoint. +2. **Environment variables** -- the Exchange key directory URL, mode flags, Exchange endpoint. No secret: the edge holds public keys only. 3. **Edge KV** -- bot patterns (updated frequently), content policy rules. 4. **CMS API** -- dynamic content policy (which paths are newly protected/released). Only fetched if CMS integration is configured. Cached at edge with TTL. @@ -235,7 +234,6 @@ npx wrangler deploy --var EXCHANGE_WBA_URL=https://exchange.example/.well-known/ |---|---| | Cloudflare | `npx wrangler deploy` | | CloudFront | Deploy CF Function + update behavior via AWS CLI or Terraform | -| Akamai | Upload EdgeWorker bundle via Akamai CLI or Control Center | | Fastly | `fastly compute deploy` | ## ACME Domain Verification Challenge Serving (v1.0) diff --git a/website/src/content/docs/components/edge-function/overview.mdx b/website/src/content/docs/components/edge-function/overview.mdx index 78671670..8fa8c2ed 100644 --- a/website/src/content/docs/components/edge-function/overview.mdx +++ b/website/src/content/docs/components/edge-function/overview.mdx @@ -126,7 +126,7 @@ The edge function serves `/.well-known/ramp.json` containing the `WellKnownManif ### Signed URL Passthrough with Agent Identity Validation -When a request arrives with signed URL parameters, the edge function verifies the signature, checks expiry, validates agent identity binding (if enabled), and enforces single-use (if enabled). See [Signed URL Verification](/components/edge-function/signed-url-verification/) for the full verification flow. +When a request arrives with signed URL parameters, the edge function verifies the signature, checks expiry, validates agent identity binding (on by default), and enforces single-use (off by default, and best-effort). See [Signed URL Verification](/components/edge-function/signed-url-verification/) for the full verification flow. ## Performance Budget @@ -151,8 +151,8 @@ Single-use disabled, everything else works Stale config, core functionality preserved | ramp.json generation fails Bot detection + 403 redirect still work - | Signing secret missing -Bot detection works, paid access broken (503) + | Verification key unavailable +Bot detection works, paid access broken (503 verify_unavailable) | Edge function itself crashes CDN serves content normally (no protection) ``` From 830fee62ee7dd81f2a27b9320da4d46d6a0cac5b Mon Sep 17 00:00:00 2001 From: noxlesh Date: Wed, 2 Sep 2026 12:46:26 +0300 Subject: [PATCH 14/16] docs(security): correct the URL TTL ceiling and the key-leak countermeasure T6 said the protocol mandates a five-minute maximum URL TTL that the provider's edge function enforces independently. No verifier enforces a second bound: the only bound is the exp the Exchange signed, so a 24-hour URL is caught by reconciliation, not refused at the edge. A provider wanting a hard ceiling has to add that check itself, and now the page says so. T7's attacker is the Exchange, and the countermeasure described the Exchange's own key custody -- a control the attacker operates cannot stop the attacker. What the design actually gives you is a bounded blast radius: a leaked verification key forges nothing, a compromised edge cannot mint URLs, and a leaked tenant key forges for one tenant only. Detection belongs to T5, where the provider's delivery log and the Exchange's transaction log join on signed_url_hash. The claim that the Exchange "signs there", inside the secrets manager, also contradicted the Exchange overview: keys load at startup and signing is a pure function call. --- website/src/content/docs/security/threat-model.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/content/docs/security/threat-model.mdx b/website/src/content/docs/security/threat-model.mdx index 1c431380..55c69c6e 100644 --- a/website/src/content/docs/security/threat-model.mdx +++ b/website/src/content/docs/security/threat-model.mdx @@ -46,11 +46,11 @@ This single pattern accounts for most ad-tech fraud: domain spoofing (self-repor ### T6: Overly long signed URL expiry **Attack**: Exchange issues 24-hour URLs instead of 5-minute, enabling reuse. -**Countermeasure (protocol)**: Protocol mandates maximum URL TTL (5 minutes). Provider's edge function enforces independently. +**Countermeasure (protocol)**: The delivery endpoint refuses any URL whose `exp` has passed, and the Exchange issues short expiries -- five minutes in the reference flows. No verifier enforces a second, independent upper bound on the TTL: the only bound is the `exp` the Exchange signed, so a 24-hour URL is detected by reconciliation (T5), not refused at the edge. A provider who wants a hard ceiling has to add that check to its own edge function. ### T7: Signing key leakage **Attack**: Exchange shares signing keys with a preferred requester, enabling URL forgery. -**Countermeasure (protocol)**: **Asymmetric signing, with per-tenant key isolation.** The Exchange holds a separate private key per tenant in a secrets manager and signs there; the delivery endpoint verifies with the matching public key, published in the Exchange's key directory. A leaked verification key forges nothing, and a compromised edge cannot mint URLs, because no signing capability is ever distributed. Sharing a signing key with one requester would require exporting a tenant private key, which no code path does. +**Countermeasure (protocol)**: **Asymmetric signing, with per-tenant key isolation.** The Exchange holds a separate private key per tenant, loaded from a key store at startup; the delivery endpoint verifies with the matching public key, published in the Exchange's key directory. This bounds the damage rather than preventing the act -- the attacker here is the Exchange, so no control the Exchange operates can stop it from exporting its own key. What the design gives you is that a leaked verification key forges nothing, a compromised edge cannot mint URLs, and a leaked tenant key forges URLs for that one tenant and no other. Detection is T5's job: forged fetches appear in the provider's delivery log and not in the Exchange's transaction log, and the two join on `signed_url_hash`. ## 2. Demand-Side Threats (Dishonest Agent / Requester) From a85f90d520b731908a299c55685f02defac20bcf Mon Sep 17 00:00:00 2001 From: noxlesh Date: Wed, 2 Sep 2026 12:47:30 +0300 Subject: [PATCH 15/16] docs(changelog): record the custody model and the two fetch paths The entry described the primitive correction but not the two facts the review of it surfaced: which half of the key pair signing_key carries and where the other half comes from, and that a custodial agent does not fetch its own content -- its identity service does, holding the same key the delivery endpoint checks the binding against. --- proto/CHANGELOG.md | 17 ++++++++++++++++- .../src/content/docs/reference/changelog.mdx | 13 +++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/proto/CHANGELOG.md b/proto/CHANGELOG.md index 70b6ef90..9064cc54 100644 --- a/proto/CHANGELOG.md +++ b/proto/CHANGELOG.md @@ -34,7 +34,22 @@ a Fastly Compute deployment runs the Ed25519 verifier. The name `edge-ed25519` is not new; it is the one ADR-012 assigned for this path. The value list also moves into the field's leading comment, because the reference page renders leading comments in preference to trailing ones and this field already had one, -so the trailing list was invisible to every reader. +so the trailing list was invisible to every reader. The field now also states +its custody model: it carries public key material, the Exchange signs with a +private key it holds and never publishes, and where the Exchange must sign with +a key the provider generated -- a CloudFront trusted key group is the provider's +own AWS resource -- the private half is provisioned out of band and never +travels in this field. Both `cdn_type` values name the tenant signing scheme +they mirror, which the previous wording asserted without saying which is which. + +The file header said "the agent is the fetcher", which held for one of the two +deployments. An agent embedding the SDK holds its own key and fetches for +itself; a custodial agent never fetches, because its key lives in its identity +service, which fetches on its behalf. Both present the same key to the delivery +endpoint, which is what makes the binding check work either way. The header's +"fully offline, no JWKS fetch required" is true of the fetcher's key, which +arrives in the request; the edge still resolves `kid` to the Exchange's public +key from a cached directory, so the claim now names the key it applies to. Two security claims were corrected beyond the primitive. The threat model said the provider holds the URL-signing private key and the Exchange calls a provider diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index 58a9604d..f7f01f6d 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -28,8 +28,17 @@ delivery endpoint record. `DomainVerificationConfirmation.cdn_type` now documents the value set `"edge-ed25519"` | `"cloudfront"`. The retired values named vendors rather than schemes, which made `"fastly"` actively wrong -- a Fastly deployment runs the -Ed25519 verifier. Akamai is no longer documented as a supported delivery target, -because EdgeAuth verifies with a secret shared with the CDN. +Ed25519 verifier. Each value now names the tenant signing scheme it mirrors. +`signing_key` states its custody model: it carries public key material, and the +Exchange signs with a private key it holds and never publishes. Akamai is no +longer documented as a supported delivery target, because EdgeAuth verifies with +a secret shared with the CDN. + +Who fetches the content is now written down. An agent embedding the SDK holds +its own key and fetches for itself; a custodial agent never fetches, because its +key lives in its identity service, which fetches on its behalf. Both present the +same key to the delivery endpoint, which is what makes the binding check work +either way. The pages said "the agent is the fetcher", which covered one case. The signed-URL verification page is rewritten against the shipped verifier, and now documents what it always did but never said: verification fails closed, From fe0384b1d0de1593c29a6aef7fc2412336ea298f Mon Sep 17 00:00:00 2001 From: noxlesh Date: Wed, 2 Sep 2026 12:50:20 +0300 Subject: [PATCH 16/16] docs(changelog): match the entry to the file's own dash style and drop an unopenable pointer The new entry used " -- " where every other entry in both files uses an em dash. It also credited an architecture record by number for the edge-ed25519 name; that record lives in the reference implementation, not here, so a reader of this repository cannot open it. The sentence now says the name was already in use without pointing at something out of reach. --- proto/CHANGELOG.md | 15 ++++++++------- website/src/content/docs/reference/changelog.mdx | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/proto/CHANGELOG.md b/proto/CHANGELOG.md index 9064cc54..525dce50 100644 --- a/proto/CHANGELOG.md +++ b/proto/CHANGELOG.md @@ -7,21 +7,21 @@ verified with its published public key, not HMAC-SHA256 over a shared secret (documentation correction; no wire change).** Since the initial public snapshot the file header, the DomainVerificationConfirmation comments and twenty website pages described a symmetric scheme with a secret shared between the Exchange and -the CDN. No implementation ever produced one: `git grep -ci hmac -- sdk/` finds +the CDN. No implementation ever produced one: `git grep -ci hmac — sdk/` finds nothing, and signing has always been a detached Ed25519 signature that a delivery endpoint verifies with a public key. The divergence was wider than the algorithm name, so an implementer following the documentation got four things wrong at once. The signed message is `"GET\n"` -followed by the canonical URL -- the whole URL with the `sig` parameter removed -and the remaining query sorted by key -- not four selected fields joined by +followed by the canonical URL — the whole URL with the `sig` parameter removed +and the remaining query sorted by key — not four selected fields joined by newlines, which means scheme, host, path and every publisher query parameter are covered too. The signature is base64url with no padding, not a hex digest. The expiry parameter is `exp`, documented as `expires`. There is a `kid` parameter the pages never mentioned, and there is no `txn_id` parameter at all. Where the documentation said `txn_id` enables three-sided reconciliation, the -join key is `signed_url_hash` -- SHA-256 of the URL verbatim -- recorded by the +join key is `signed_url_hash` — SHA-256 of the URL verbatim — recorded by the Exchange on the transaction and by the delivery endpoint on its delivery event. Neither side chooses the value. @@ -31,14 +31,15 @@ reading that matched the implementation. Its format now follows `cdn_type`, whos documented value set becomes `"edge-ed25519"` | `"cloudfront"`. The retired values named vendors rather than schemes, which made `"fastly"` actively wrong -- a Fastly Compute deployment runs the Ed25519 verifier. The name `edge-ed25519` -is not new; it is the one ADR-012 assigned for this path. The value list also +is not new; the reference implementation's architecture records already use it +for this path. The value list also moves into the field's leading comment, because the reference page renders leading comments in preference to trailing ones and this field already had one, so the trailing list was invisible to every reader. The field now also states its custody model: it carries public key material, the Exchange signs with a private key it holds and never publishes, and where the Exchange must sign with -a key the provider generated -- a CloudFront trusted key group is the provider's -own AWS resource -- the private half is provisioned out of band and never +a key the provider generated — a CloudFront trusted key group is the provider's +own AWS resource — the private half is provisioned out of band and never travels in this field. Both `cdn_type` values name the tenant signing scheme they mirror, which the previous wording asserted without saying which is which. diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index f7f01f6d..e39f9eff 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -16,8 +16,8 @@ secret shared between the Exchange and the CDN. No implementation ever produced one, and the SDK published from this repository has never contained an HMAC. An implementer following the documentation got four things wrong at once. The -signed message is `"GET\n"` followed by the canonical URL -- the whole URL with -the `sig` parameter removed and the remaining query sorted by key -- so scheme, +signed message is `"GET\n"` followed by the canonical URL — the whole URL with +the `sig` parameter removed and the remaining query sorted by key — so scheme, host, path and every publisher query parameter are covered, not four selected fields joined by newlines. The signature is base64url with no padding, not a hex digest. The expiry parameter is `exp`, documented as `expires`. There is a `kid` @@ -27,7 +27,7 @@ delivery endpoint record. `DomainVerificationConfirmation.cdn_type` now documents the value set `"edge-ed25519"` | `"cloudfront"`. The retired values named vendors rather than -schemes, which made `"fastly"` actively wrong -- a Fastly deployment runs the +schemes, which made `"fastly"` actively wrong — a Fastly deployment runs the Ed25519 verifier. Each value now names the tenant signing scheme it mirrors. `signing_key` states its custody model: it carries public key material, and the Exchange signs with a private key it holds and never publishes. Akamai is no