@@ -6,14 +6,17 @@ import (
66 "crypto/x509"
77 "encoding/pem"
88 "fmt"
9+ "hash/crc32"
910 "sync/atomic"
1011 "testing"
12+ "time"
1113
1214 "cloud.google.com/go/kms/apiv1/kmspb"
1315 "github.com/stretchr/testify/assert"
1416 "github.com/stretchr/testify/require"
1517 "google.golang.org/grpc/codes"
1618 "google.golang.org/grpc/status"
19+ "google.golang.org/protobuf/types/known/wrapperspb"
1720)
1821
1922// mockKMSClient is a test double implementing KMSClient.
@@ -98,19 +101,26 @@ func TestSign_Success(t *testing.T) {
98101 _ , publicKeyPEM := generateTestEd25519PEM (t )
99102
100103 expectedSig := []byte ("test-signature-bytes" )
104+ expectedMsg := []byte ("hello world" )
101105 mock := & mockKMSClient {
102106 publicKeyPEM : publicKeyPEM ,
103107 signFn : func (_ context.Context , req * kmspb.AsymmetricSignRequest ) (* kmspb.AsymmetricSignResponse , error ) {
104108 assert .Equal (t , "projects/p/locations/global/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1" , req .Name )
105- assert .Equal (t , []byte ("hello world" ), req .Data )
106- return & kmspb.AsymmetricSignResponse {Signature : expectedSig }, nil
109+ assert .Equal (t , expectedMsg , req .Data )
110+ require .NotNil (t , req .DataCrc32C )
111+ assert .Equal (t , int64 (crc32 .Checksum (expectedMsg , castagnoliTable )), req .DataCrc32C .GetValue ())
112+ return & kmspb.AsymmetricSignResponse {
113+ Signature : expectedSig ,
114+ VerifiedDataCrc32C : true ,
115+ SignatureCrc32C : wrapperspb .Int64 (int64 (crc32 .Checksum (expectedSig , castagnoliTable ))),
116+ }, nil
107117 },
108118 }
109119
110120 s , err := kmsSignerFromClient (context .Background (), mock , "projects/p/locations/global/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1" , nil )
111121 require .NoError (t , err )
112122
113- sig , err := s .Sign (context .Background (), [] byte ( "hello world" ) )
123+ sig , err := s .Sign (context .Background (), expectedMsg )
114124 require .NoError (t , err )
115125 assert .Equal (t , expectedSig , sig )
116126}
@@ -177,6 +187,125 @@ func TestSign_NonRetryableError_NoRetries(t *testing.T) {
177187 assert .Equal (t , int32 (1 ), atomic .LoadInt32 (& calls ), "non-retryable errors should fail fast" )
178188}
179189
190+ func TestSign_IntegrityCheckVerifiedDataFalse_RetriesAndFails (t * testing.T ) {
191+ _ , publicKeyPEM := generateTestEd25519PEM (t )
192+
193+ var calls int32
194+ mock := & mockKMSClient {
195+ publicKeyPEM : publicKeyPEM ,
196+ signFn : func (_ context.Context , _ * kmspb.AsymmetricSignRequest ) (* kmspb.AsymmetricSignResponse , error ) {
197+ atomic .AddInt32 (& calls , 1 )
198+ sig := []byte ("sig" )
199+ return & kmspb.AsymmetricSignResponse {
200+ Signature : sig ,
201+ VerifiedDataCrc32C : false ,
202+ SignatureCrc32C : wrapperspb .Int64 (int64 (crc32 .Checksum (sig , castagnoliTable ))),
203+ }, nil
204+ },
205+ }
206+
207+ s , err := kmsSignerFromClient (
208+ context .Background (),
209+ mock ,
210+ "projects/p/locations/global/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1" ,
211+ & Options {MaxRetries : 1 },
212+ )
213+ require .NoError (t , err )
214+
215+ _ , err = s .Sign (context .Background (), []byte ("hello world" ))
216+ require .Error (t , err )
217+ assert .Contains (t , err .Error (), "verified_data_crc32c is false" )
218+ assert .Equal (t , int32 (2 ), atomic .LoadInt32 (& calls ), "integrity failures should be retried" )
219+ }
220+
221+ func TestSign_IntegrityCheckSignatureCRC32CMismatch_RetriesAndFails (t * testing.T ) {
222+ _ , publicKeyPEM := generateTestEd25519PEM (t )
223+
224+ var calls int32
225+ mock := & mockKMSClient {
226+ publicKeyPEM : publicKeyPEM ,
227+ signFn : func (_ context.Context , _ * kmspb.AsymmetricSignRequest ) (* kmspb.AsymmetricSignResponse , error ) {
228+ atomic .AddInt32 (& calls , 1 )
229+ return & kmspb.AsymmetricSignResponse {
230+ Signature : []byte ("sig" ),
231+ VerifiedDataCrc32C : true ,
232+ SignatureCrc32C : wrapperspb .Int64 (12345 ),
233+ }, nil
234+ },
235+ }
236+
237+ s , err := kmsSignerFromClient (
238+ context .Background (),
239+ mock ,
240+ "projects/p/locations/global/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1" ,
241+ & Options {MaxRetries : 1 },
242+ )
243+ require .NoError (t , err )
244+
245+ _ , err = s .Sign (context .Background (), []byte ("hello world" ))
246+ require .Error (t , err )
247+ assert .Contains (t , err .Error (), "signature_crc32c mismatch" )
248+ assert .Equal (t , int32 (2 ), atomic .LoadInt32 (& calls ), "integrity failures should be retried" )
249+ }
250+
251+ func TestSign_IntegrityCheckRecoversOnRetry (t * testing.T ) {
252+ _ , publicKeyPEM := generateTestEd25519PEM (t )
253+
254+ var calls int32
255+ expectedSig := []byte ("valid-signature" )
256+ mock := & mockKMSClient {
257+ publicKeyPEM : publicKeyPEM ,
258+ signFn : func (_ context.Context , _ * kmspb.AsymmetricSignRequest ) (* kmspb.AsymmetricSignResponse , error ) {
259+ attempt := atomic .AddInt32 (& calls , 1 )
260+ if attempt == 1 {
261+ return & kmspb.AsymmetricSignResponse {
262+ Signature : []byte ("corrupted" ),
263+ VerifiedDataCrc32C : false ,
264+ SignatureCrc32C : wrapperspb .Int64 (1 ),
265+ }, nil
266+ }
267+ return & kmspb.AsymmetricSignResponse {
268+ Signature : expectedSig ,
269+ VerifiedDataCrc32C : true ,
270+ SignatureCrc32C : wrapperspb .Int64 (int64 (crc32 .Checksum (expectedSig , castagnoliTable ))),
271+ }, nil
272+ },
273+ }
274+
275+ s , err := kmsSignerFromClient (
276+ context .Background (),
277+ mock ,
278+ "projects/p/locations/global/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1" ,
279+ & Options {MaxRetries : 2 },
280+ )
281+ require .NoError (t , err )
282+
283+ got , err := s .Sign (context .Background (), []byte ("hello world" ))
284+ require .NoError (t , err )
285+ assert .Equal (t , expectedSig , got )
286+ assert .Equal (t , int32 (2 ), atomic .LoadInt32 (& calls ), "second attempt should succeed" )
287+ }
288+
289+ func TestRetryBackoff_Capped (t * testing.T ) {
290+ testCases := []struct {
291+ name string
292+ attempt int
293+ expected time.Duration
294+ }{
295+ {name : "attempt 1" , attempt : 1 , expected : 100 * time .Millisecond },
296+ {name : "attempt 2" , attempt : 2 , expected : 200 * time .Millisecond },
297+ {name : "attempt 6" , attempt : 6 , expected : 3200 * time .Millisecond },
298+ {name : "attempt 7 capped" , attempt : 7 , expected : 5 * time .Second },
299+ {name : "attempt 10 capped" , attempt : 10 , expected : 5 * time .Second },
300+ }
301+
302+ for _ , tc := range testCases {
303+ t .Run (tc .name , func (t * testing.T ) {
304+ assert .Equal (t , tc .expected , retryBackoff (tc .attempt ))
305+ })
306+ }
307+ }
308+
180309func TestGetPublic_Cached (t * testing.T ) {
181310 pub , publicKeyPEM := generateTestEd25519PEM (t )
182311
0 commit comments