Skip to content

Commit 7949339

Browse files
committed
fix(attestation): decode DSSE signature at bundle source
Move the double-base64 fix into BundleFromDSSEEnvelope so new Sigstore bundles carry a properly decoded signature by construction. FixSignatureInBundle is retained in the verifier for backward compatibility with attestations stored before the fix. Promote the envelope-to-bundle test helper to the shared testhelpers package so other biz integration tests can reuse it. Follow-up to #3055. Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
1 parent 66fd617 commit 7949339

5 files changed

Lines changed: 151 additions & 22 deletions

File tree

app/cli/pkg/action/attestation_push.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"time"
2626

2727
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
28-
"github.com/chainloop-dev/chainloop/pkg/attestation"
2928
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
3029
v1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
3130
"github.com/chainloop-dev/chainloop/pkg/attestation/renderer"
@@ -304,8 +303,6 @@ func (action *AttestationPush) saveBundle(bundle *protobundle.Bundle) error {
304303
}
305304

306305
func pushToControlPlane(ctx context.Context, conn *grpc.ClientConn, bundle *protobundle.Bundle, workflowRunID string, markVersionAsReleased bool) (string, error) {
307-
// remove additional base64 encoding in signature. See https://github.com/chainloop-dev/chainloop/issues/1832
308-
attestation.FixSignatureInBundle(bundle)
309306
encodedBundle, err := encodeBundle(bundle)
310307
if err != nil {
311308
return "", fmt.Errorf("encoding attestation: %w", err)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// Copyright 2026 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package testhelpers
17+
18+
import (
19+
"encoding/json"
20+
"os"
21+
"testing"
22+
23+
"github.com/chainloop-dev/chainloop/pkg/attestation"
24+
"github.com/secure-systems-lab/go-securesystemslib/dsse"
25+
"github.com/stretchr/testify/require"
26+
"google.golang.org/protobuf/encoding/protojson"
27+
)
28+
29+
// BundleBytesFromEnvelope reads a DSSE envelope fixture from disk, wraps it into a
30+
// Sigstore bundle, and returns its protojson-encoded bytes. Useful to feed envelope
31+
// test fixtures into APIs that now only accept Sigstore bundles.
32+
func BundleBytesFromEnvelope(t *testing.T, path string) []byte {
33+
t.Helper()
34+
raw, err := os.ReadFile(path)
35+
require.NoError(t, err)
36+
var env dsse.Envelope
37+
require.NoError(t, json.Unmarshal(raw, &env))
38+
b, err := attestation.BundleFromDSSEEnvelope(&env)
39+
require.NoError(t, err)
40+
out, err := protojson.Marshal(b)
41+
require.NoError(t, err)
42+
return out
43+
}

app/controlplane/pkg/biz/workflowrun_integration_test.go

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -468,18 +468,6 @@ func testBundle(t *testing.T, path string) (*v1.Bundle, []byte) {
468468
return &bundle, bundleJSON
469469
}
470470

471-
// testBundleBytesFromEnvelope wraps a DSSE envelope file into a Sigstore bundle and returns its protojson bytes.
472-
func testBundleBytesFromEnvelope(t *testing.T, path string) []byte {
473-
_, envBytes := testEnvelope(t, path)
474-
var env dsse.Envelope
475-
require.NoError(t, json.Unmarshal(envBytes, &env))
476-
b, err := attestation.BundleFromDSSEEnvelope(&env)
477-
require.NoError(t, err)
478-
out, err := protojson.Marshal(b)
479-
require.NoError(t, err)
480-
return out
481-
}
482-
483471
const (
484472
version1 = "v1"
485473
version2 = "v2"
@@ -519,7 +507,7 @@ func setupWorkflowRunTestData(t *testing.T, suite *testhelpers.TestingUseCases,
519507
ProjectVersion: version1,
520508
})
521509
assert.NoError(err)
522-
bundleBytes := testBundleBytesFromEnvelope(t, "testdata/attestations/full.json")
510+
bundleBytes := testhelpers.BundleBytesFromEnvelope(t, "testdata/attestations/full.json")
523511
d, err := suite.WorkflowRun.SaveAttestation(ctx, s.runOrg1.ID.String(), bundleBytes)
524512
assert.NoError(err)
525513
s.digestAtt1 = d.String()
@@ -530,7 +518,7 @@ func setupWorkflowRunTestData(t *testing.T, suite *testhelpers.TestingUseCases,
530518
ProjectVersion: version1,
531519
})
532520
assert.NoError(err)
533-
bundleBytes = testBundleBytesFromEnvelope(t, "testdata/attestations/empty.json")
521+
bundleBytes = testhelpers.BundleBytesFromEnvelope(t, "testdata/attestations/empty.json")
534522
d, err = suite.WorkflowRun.SaveAttestation(ctx, s.runOrg2.ID.String(), bundleBytes)
535523
assert.NoError(err)
536524
s.digestAttOrg2 = d.String()
@@ -541,7 +529,7 @@ func setupWorkflowRunTestData(t *testing.T, suite *testhelpers.TestingUseCases,
541529
ProjectVersion: version2,
542530
})
543531
assert.NoError(err)
544-
bundleBytes = testBundleBytesFromEnvelope(t, "testdata/attestations/with-string.json")
532+
bundleBytes = testhelpers.BundleBytesFromEnvelope(t, "testdata/attestations/with-string.json")
545533
d, err = suite.WorkflowRun.SaveAttestation(ctx, s.runOrg2Public.ID.String(), bundleBytes)
546534
assert.NoError(err)
547535
s.digestAttPublic = d.String()

pkg/attestation/attestations.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,15 @@ func DSSEEnvelopeFromBundle(bundle *protobundle.Bundle) *dsse.Envelope {
7070
}
7171

7272
func BundleFromDSSEEnvelope(dsseEnvelope *dsse.Envelope) (*protobundle.Bundle, error) {
73-
// DSSE Envelope is already base64 encoded, we need to decode to prevent it from being encoded twice
73+
// DSSE Envelope payload and signature are base64 encoded, we need to decode them so they
74+
// are not encoded twice when stored as raw bytes in the Sigstore bundle. See #1832.
7475
payload, err := base64.StdEncoding.DecodeString(dsseEnvelope.Payload)
7576
if err != nil {
76-
return nil, fmt.Errorf("decoding: %w", err)
77+
return nil, fmt.Errorf("decoding payload: %w", err)
78+
}
79+
sig, err := base64.StdEncoding.DecodeString(dsseEnvelope.Signatures[0].Sig)
80+
if err != nil {
81+
return nil, fmt.Errorf("decoding signature: %w", err)
7782
}
7883
return &protobundle.Bundle{
7984
MediaType: "application/vnd.dev.sigstore.bundle+json;version=0.3",
@@ -82,7 +87,7 @@ func BundleFromDSSEEnvelope(dsseEnvelope *dsse.Envelope) (*protobundle.Bundle, e
8287
PayloadType: dsseEnvelope.PayloadType,
8388
Signatures: []*sigstoredsse.Signature{
8489
{
85-
Sig: []byte(dsseEnvelope.Signatures[0].Sig),
90+
Sig: sig,
8691
Keyid: dsseEnvelope.Signatures[0].KeyID,
8792
},
8893
},
@@ -106,7 +111,9 @@ func DSSEEnvelopeFromBundleBytes(bundle []byte) (*dsse.Envelope, error) {
106111
}
107112

108113
// FixSignatureInBundle removes any additional base64 encoding from the signature in the bundle.
109-
// Old attestations have signatures base64 encoded twice, see https://github.com/chainloop-dev/chainloop/issues/1832
114+
// Kept for backward compatibility with attestations stored before the fix for
115+
// https://github.com/chainloop-dev/chainloop/issues/1832, whose signatures are base64-encoded twice.
116+
// New bundles produced by BundleFromDSSEEnvelope already carry a properly decoded signature.
110117
func FixSignatureInBundle(bundle *protobundle.Bundle) {
111118
sig := bundle.GetDsseEnvelope().GetSignatures()[0].GetSig()
112119
dst := make([]byte, base64.StdEncoding.EncodedLen(len(sig)))
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//
2+
// Copyright 2026 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package attestation_test
17+
18+
import (
19+
"encoding/base64"
20+
"testing"
21+
22+
"github.com/chainloop-dev/chainloop/pkg/attestation"
23+
"github.com/secure-systems-lab/go-securesystemslib/dsse"
24+
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
26+
)
27+
28+
// Guards against the double-base64 bug in https://github.com/chainloop-dev/chainloop/issues/1832.
29+
func TestBundleFromDSSEEnvelopeDecodesSignature(t *testing.T) {
30+
rawSig := []byte{0x30, 0x44, 0x02, 0x20, 0xAA, 0xBB, 0xCC, 0xDD}
31+
rawPayload := []byte(`{"_type":"statement"}`)
32+
33+
env := &dsse.Envelope{
34+
PayloadType: "application/vnd.in-toto+json",
35+
Payload: base64.StdEncoding.EncodeToString(rawPayload),
36+
Signatures: []dsse.Signature{
37+
{KeyID: "key-1", Sig: base64.StdEncoding.EncodeToString(rawSig)},
38+
},
39+
}
40+
41+
bundle, err := attestation.BundleFromDSSEEnvelope(env)
42+
require.NoError(t, err)
43+
44+
gotEnv := bundle.GetDsseEnvelope()
45+
assert.Equal(t, rawPayload, gotEnv.GetPayload())
46+
require.Len(t, gotEnv.GetSignatures(), 1)
47+
assert.Equal(t, rawSig, gotEnv.GetSignatures()[0].GetSig())
48+
assert.Equal(t, "key-1", gotEnv.GetSignatures()[0].GetKeyid())
49+
}
50+
51+
func TestBundleRoundTripWithFixedSignature(t *testing.T) {
52+
rawSig := []byte{0x30, 0x44, 0x02, 0x20, 0xAA, 0xBB, 0xCC, 0xDD}
53+
encodedSig := base64.StdEncoding.EncodeToString(rawSig)
54+
55+
env := &dsse.Envelope{
56+
PayloadType: "application/vnd.in-toto+json",
57+
Payload: base64.StdEncoding.EncodeToString([]byte("payload")),
58+
Signatures: []dsse.Signature{
59+
{KeyID: "key-1", Sig: encodedSig},
60+
},
61+
}
62+
63+
bundle, err := attestation.BundleFromDSSEEnvelope(env)
64+
require.NoError(t, err)
65+
66+
before := bundle.GetDsseEnvelope().GetSignatures()[0].GetSig()
67+
attestation.FixSignatureInBundle(bundle)
68+
assert.Equal(t, before, bundle.GetDsseEnvelope().GetSignatures()[0].GetSig(),
69+
"FixSignatureInBundle should be a no-op on properly formed bundles")
70+
71+
gotEnv := attestation.DSSEEnvelopeFromBundle(bundle)
72+
assert.Equal(t, encodedSig, gotEnv.Signatures[0].Sig)
73+
}
74+
75+
func TestFixSignatureInBundleRepairsLegacyBundles(t *testing.T) {
76+
rawSig := []byte{0x30, 0x44, 0x02, 0x20, 0xAA, 0xBB, 0xCC, 0xDD}
77+
encodedSig := base64.StdEncoding.EncodeToString(rawSig)
78+
79+
env := &dsse.Envelope{
80+
PayloadType: "application/vnd.in-toto+json",
81+
Payload: base64.StdEncoding.EncodeToString([]byte("payload")),
82+
Signatures: []dsse.Signature{
83+
{KeyID: "key-1", Sig: encodedSig},
84+
},
85+
}
86+
bundle, err := attestation.BundleFromDSSEEnvelope(env)
87+
require.NoError(t, err)
88+
89+
// Simulate the legacy bug: signature is stored as the ASCII bytes of the base64 string.
90+
bundle.GetDsseEnvelope().GetSignatures()[0].Sig = []byte(encodedSig)
91+
92+
attestation.FixSignatureInBundle(bundle)
93+
assert.Equal(t, rawSig, bundle.GetDsseEnvelope().GetSignatures()[0].GetSig())
94+
}

0 commit comments

Comments
 (0)