@@ -54,6 +54,28 @@ type ReleaseAutomation = {
5454 integrity : string ;
5555 sha256 : string ;
5656 } ;
57+ verifyGitHubPublishedRelease : (
58+ metadata : ReleaseMetadata ,
59+ archive : Uint8Array ,
60+ expected : {
61+ version : string ;
62+ gitHead : string ;
63+ repository : string ;
64+ runId : string ;
65+ } ,
66+ provenance : {
67+ version : string ;
68+ gitHead : string ;
69+ repository : string ;
70+ runId : string ;
71+ sha512 : string ;
72+ } ,
73+ ) => {
74+ version : string ;
75+ gitHead : string ;
76+ integrity : string ;
77+ sha256 : string ;
78+ } ;
5779 verifySignatureAudit : (
5880 report : ReleaseMetadata ,
5981 archive : Uint8Array ,
@@ -108,6 +130,7 @@ const {
108130 requirePublishedReleaseIncrease,
109131 releaseHistory,
110132 verifyPublishedRelease,
133+ verifyGitHubPublishedRelease,
111134 verifySignatureAudit,
112135 verifyRecoveredSignatureAudit,
113136 verifyGitHubRelease,
@@ -744,6 +767,111 @@ describe("published npm release verification", () => {
744767 } ) ;
745768} ) ;
746769
770+ describe ( "GitHub release publication verification" , ( ) => {
771+ const expected = {
772+ version : "0.1.2" ,
773+ gitHead : releaseCommit ,
774+ repository : releaseRepository ,
775+ runId : releaseRun ,
776+ } ;
777+ const provenance = {
778+ ...expected ,
779+ sha512,
780+ } ;
781+
782+ test ( "retains strict source and artifact verification for current releases" , ( ) => {
783+ expect (
784+ verifyGitHubPublishedRelease (
785+ publishedMetadata ( ) ,
786+ archive ,
787+ expected ,
788+ provenance ,
789+ ) ,
790+ ) . toEqual ( {
791+ version : "0.1.2" ,
792+ gitHead : releaseCommit ,
793+ integrity,
794+ sha256 : digest . slice ( "sha256:" . length ) ,
795+ } ) ;
796+ } ) ;
797+
798+ test . each ( [ "0.1.0" , "0.1.1" ] ) (
799+ "recovers the missing gitHead only from verified provenance for %s" ,
800+ ( version ) => {
801+ const metadata : ReleaseMetadata = { ...publishedMetadata ( ) , version } ;
802+ delete metadata [ "gitHead" ] ;
803+
804+ expect (
805+ verifyGitHubPublishedRelease (
806+ metadata ,
807+ archive ,
808+ { ...expected , version } ,
809+ { ...provenance , version } ,
810+ ) ,
811+ ) . toEqual ( {
812+ version,
813+ gitHead : releaseCommit ,
814+ integrity,
815+ sha256 : digest . slice ( "sha256:" . length ) ,
816+ } ) ;
817+
818+ expect ( ( ) =>
819+ verifyPublishedRelease ( metadata , archive , { ...expected , version } ) ,
820+ ) . toThrow ( "npm package gitHead must match release commit" ) ;
821+ } ,
822+ ) ;
823+
824+ test ( "rejects a missing gitHead on all later npm releases" , ( ) => {
825+ const metadata : ReleaseMetadata = { ...publishedMetadata ( ) } ;
826+ delete metadata [ "gitHead" ] ;
827+
828+ expect ( ( ) =>
829+ verifyGitHubPublishedRelease ( metadata , archive , expected , provenance ) ,
830+ ) . toThrow ( "Only npm releases 0.1.0 and 0.1.1 may omit gitHead." ) ;
831+ } ) ;
832+
833+ test ( "rejects a mismatched historical npm gitHead" , ( ) => {
834+ expect ( ( ) =>
835+ verifyGitHubPublishedRelease (
836+ {
837+ ...publishedMetadata ( ) ,
838+ version : "0.1.0" ,
839+ gitHead : "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ,
840+ } ,
841+ archive ,
842+ { ...expected , version : "0.1.0" } ,
843+ { ...provenance , version : "0.1.0" } ,
844+ ) ,
845+ ) . toThrow ( "npm package gitHead must match release commit" ) ;
846+ } ) ;
847+
848+ test . each ( [
849+ { version : "0.1.1" } ,
850+ { gitHead : "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } ,
851+ { repository : "different/codex-security" } ,
852+ { runId : "30481596228" } ,
853+ { sha512 : "0" . repeat ( 128 ) } ,
854+ ] ) ( "rejects signed provenance that does not match %j" , ( mismatch ) => {
855+ expect ( ( ) =>
856+ verifyGitHubPublishedRelease ( publishedMetadata ( ) , archive , expected , {
857+ ...provenance ,
858+ ...mismatch ,
859+ } ) ,
860+ ) . toThrow ( "Verified signed npm provenance must match the GitHub release." ) ;
861+ } ) ;
862+
863+ test ( "rejects missing verified provenance" , ( ) => {
864+ expect ( ( ) =>
865+ verifyGitHubPublishedRelease (
866+ publishedMetadata ( ) ,
867+ archive ,
868+ expected ,
869+ undefined as unknown as typeof provenance ,
870+ ) ,
871+ ) . toThrow ( "Verified signed npm provenance must match the GitHub release." ) ;
872+ } ) ;
873+ } ) ;
874+
747875describe ( "cryptographically verified npm provenance" , ( ) => {
748876 test ( "binds the verified bundle to the exact archive, source, and run" , ( ) => {
749877 expect (
@@ -2129,7 +2257,9 @@ describe("GitHub release workflow safeguards", () => {
21292257 ' if [[ "${1:-}" == "sdk/typescript/scripts/release-automation.mjs" ]]; then' ,
21302258 " cat >/dev/null" ,
21312259 ' case "$2" in' ,
2132- " verify-publication)" ,
2260+ " verify-github-publication)" ,
2261+ ' if [[ -z "${CODEX_SECURITY_VERIFIED_PROVENANCE:-}" ||' ,
2262+ ' "$6" != "$GITHUB_REPOSITORY" ]]; then return 69; fi' ,
21332263 " printf '%s\\n' 'verified published artifact'" ,
21342264 " ;;" ,
21352265 " verify-provenance)" ,
0 commit comments