@@ -914,6 +914,169 @@ func TestAttestationDataMarshalJSON(t *testing.T) {
914914 }
915915}
916916
917+ func TestParseAttestationsFromBundles (t * testing.T ) {
918+ ref := name .MustParseReference ("registry.io/repository/image:tag" )
919+
920+ slsaV02Statement := in_toto.ProvenanceStatementSLSA02 {
921+ //nolint:staticcheck
922+ StatementHeader : in_toto.StatementHeader {
923+ Type : in_toto .StatementInTotoV01 ,
924+ PredicateType : v02 .PredicateSLSAProvenance ,
925+ //nolint:staticcheck
926+ Subject : []in_toto.Subject {
927+ {Name : "test-image" , Digest : common.DigestSet {"sha256" : "abc123" }},
928+ },
929+ },
930+ Predicate : v02.ProvenancePredicate {
931+ BuildType : pipelineRunBuildType ,
932+ Builder : common.ProvenanceBuilder {ID : "https://tekton.dev/chains/v2" },
933+ },
934+ }
935+
936+ cases := []struct {
937+ name string
938+ layers []oci.Signature
939+ expectErr bool
940+ errContains string
941+ expectedAttCount int
942+ expectedPredTypes []string
943+ }{
944+ {
945+ name : "empty layers" ,
946+ layers : []oci.Signature {},
947+ expectedAttCount : 0 ,
948+ },
949+ {
950+ name : "single valid bundle attestation" ,
951+ layers : []oci.Signature {createBundleDSSESignature (t , slsaV02Statement )},
952+ expectedAttCount : 1 ,
953+ expectedPredTypes : []string {v02 .PredicateSLSAProvenance },
954+ },
955+ {
956+ name : "multiple valid bundle attestations" ,
957+ layers : []oci.Signature {
958+ createBundleDSSESignature (t , slsaV02Statement ),
959+ createBundleDSSESignature (t , slsaV02Statement ),
960+ },
961+ expectedAttCount : 2 ,
962+ expectedPredTypes : []string {v02 .PredicateSLSAProvenance , v02 .PredicateSLSAProvenance },
963+ },
964+ {
965+ name : "skips non-intoto payload type" ,
966+ layers : func () []oci.Signature {
967+ payload := `{"payloadType":"application/octet-stream","payload":"aGVsbG8="}`
968+ sig , err := static .NewSignature ([]byte (payload ), "test-sig" )
969+ require .NoError (t , err )
970+ return []oci.Signature {sig }
971+ }(),
972+ expectedAttCount : 0 ,
973+ },
974+ {
975+ name : "skips invalid JSON payload" ,
976+ layers : func () []oci.Signature {
977+ sig , err := static .NewSignature ([]byte (`not-json` ), "test-sig" )
978+ require .NoError (t , err )
979+ return []oci.Signature {sig }
980+ }(),
981+ expectedAttCount : 0 ,
982+ },
983+ {
984+ name : "valid attestation with populated signatures" ,
985+ layers : []oci.Signature {createBundleDSSESignature (t , slsaV02Statement )},
986+ expectedAttCount : 1 ,
987+ },
988+ }
989+
990+ for _ , tc := range cases {
991+ t .Run (tc .name , func (t * testing.T ) {
992+ a := ApplicationSnapshotImage {reference : ref }
993+
994+ err := a .parseAttestationsFromBundles (tc .layers )
995+
996+ if tc .expectErr {
997+ require .Error (t , err )
998+ if tc .errContains != "" {
999+ assert .Contains (t , err .Error (), tc .errContains )
1000+ }
1001+ } else {
1002+ require .NoError (t , err )
1003+ assert .Equal (t , tc .expectedAttCount , len (a .attestations ))
1004+
1005+ for i , expectedType := range tc .expectedPredTypes {
1006+ assert .Equal (t , expectedType , a .attestations [i ].PredicateType ())
1007+ }
1008+
1009+ for _ , att := range a .attestations {
1010+ assert .NotNil (t , att .Signatures (), "bundle attestations should have signatures populated" )
1011+ }
1012+ }
1013+ })
1014+ }
1015+ }
1016+
1017+ func TestParseAttestationsFromBundlesPopulatesSignatures (t * testing.T ) {
1018+ ref := name .MustParseReference ("registry.io/repository/image:tag" )
1019+
1020+ //nolint:staticcheck
1021+ statement := in_toto.Statement {
1022+ //nolint:staticcheck
1023+ StatementHeader : in_toto.StatementHeader {
1024+ Type : in_toto .StatementInTotoV01 ,
1025+ PredicateType : "https://example.com/test/v1" ,
1026+ //nolint:staticcheck
1027+ Subject : []in_toto.Subject {
1028+ {Name : "test" , Digest : common.DigestSet {"sha256" : "abc" }},
1029+ },
1030+ },
1031+ Predicate : json .RawMessage (`{"test":"data"}` ),
1032+ }
1033+
1034+ a := ApplicationSnapshotImage {reference : ref }
1035+ layers := []oci.Signature {createBundleDSSESignature (t , statement )}
1036+
1037+ err := a .parseAttestationsFromBundles (layers )
1038+ require .NoError (t , err )
1039+ require .Len (t , a .attestations , 1 )
1040+
1041+ sigs := a .attestations [0 ].Signatures ()
1042+ assert .NotEmpty (t , sigs , "attestation signatures should be populated for bundle attestations" )
1043+ assert .NotEmpty (t , sigs [0 ].Certificate , "attestation signature should have certificate" )
1044+ }
1045+
1046+ // createBundleDSSESignature creates a test signature mimicking the bundle
1047+ // format: a raw DSSE envelope accessible via Payload().
1048+ func createBundleDSSESignature (t * testing.T , statement any ) oci.Signature {
1049+ t .Helper ()
1050+
1051+ statementJSON , err := json .Marshal (statement )
1052+ require .NoError (t , err )
1053+
1054+ encodedStatement := base64 .StdEncoding .EncodeToString (statementJSON )
1055+
1056+ dsseEnvelope := dsse.Envelope {
1057+ Payload : encodedStatement ,
1058+ PayloadType : "application/vnd.in-toto+json" ,
1059+ Signatures : []dsse.Signature {
1060+ {KeyID : "test-key" , Sig : "dGVzdC1zaWduYXR1cmU=" },
1061+ },
1062+ }
1063+
1064+ payload , err := json .Marshal (dsseEnvelope )
1065+ require .NoError (t , err )
1066+
1067+ sig , err := static .NewSignature (
1068+ payload ,
1069+ "test-signature" ,
1070+ static .WithCertChain (
1071+ signature .ChainguardReleaseCert ,
1072+ signature .SigstoreChainCert ,
1073+ ),
1074+ )
1075+ require .NoError (t , err )
1076+
1077+ return sig
1078+ }
1079+
9171080// createDSSESignature creates a test signature with a DSSE envelope containing the given statement
9181081func createDSSESignature (t * testing.T , statement any ) oci.Signature {
9191082 t .Helper ()
0 commit comments