From e5144c554e2f81edec71c1f6d1671a9ab4f3e683 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 17 Mar 2026 08:12:29 -0700 Subject: [PATCH 01/12] fix(sdk): reclassify ErrRewrapBadRequest away from ErrTampered ErrRewrapBadRequest (KAS 400) was wrapped under ErrTampered, causing errors.Is(err, ErrTampered) to produce false positives on client/config errors. Introduce ErrKASRequestError as the new parent sentinel for KAS-originated request failures, keeping integrity and request errors in separate hierarchies. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- sdk/tdf_test.go | 2 ++ sdk/tdferrors.go | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index b1d5c0acf3..3f6b0f59b4 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -3139,7 +3139,9 @@ func TestGetKasErrorToReturn(t *testing.T) { inputError := errors.New("rpc error: code = InvalidArgument desc = invalid request") result := getKasErrorToReturn(inputError, defaultError) require.ErrorIs(t, result, ErrRewrapBadRequest) + require.ErrorIs(t, result, ErrKASRequestError) require.ErrorIs(t, result, defaultError) + require.NotErrorIs(t, result, ErrTampered, "KAS 400 must not match ErrTampered") }) t.Run("PermissionDenied error returns ErrRewrapForbidden", func(t *testing.T) { diff --git a/sdk/tdferrors.go b/sdk/tdferrors.go index f19331fa82..8bf1a996f2 100644 --- a/sdk/tdferrors.go +++ b/sdk/tdferrors.go @@ -18,9 +18,12 @@ var ( ErrSegSigValidation = fmt.Errorf("[%w] tdf: failed integrity check on segment hash", ErrTampered) ErrTDFPayloadReadFail = fmt.Errorf("[%w] tdf: fail to read payload from tdf", ErrTampered) ErrTDFPayloadInvalidOffset = fmt.Errorf("[%w] sdk.Reader.ReadAt: negative offset", ErrTampered) - ErrRewrapBadRequest = fmt.Errorf("[%w] tdf: rewrap request 400", ErrTampered) ErrRootSignatureFailure = fmt.Errorf("[%w] tdf: issue verifying root signature", ErrTampered) - ErrRewrapForbidden = errors.New("tdf: rewrap request 403") + + // KAS request errors — client/configuration issues, not integrity failures + ErrKASRequestError = errors.New("tdf: KAS request error") + ErrRewrapBadRequest = fmt.Errorf("[%w] tdf: rewrap request 400", ErrKASRequestError) + ErrRewrapForbidden = errors.New("tdf: rewrap request 403") ) // Custom error struct for Assertion errors From c9688530ae161c6859e3c4710b019624bac81f08 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 17 Mar 2026 08:21:00 -0700 Subject: [PATCH 02/12] fix(sdk): wrap ErrRewrapForbidden under ErrKASRequestError Group all KAS request errors under ErrKASRequestError so consumers can catch both 400 and 403 KAS failures with errors.Is(err, ErrKASRequestError). Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- sdk/tdf_test.go | 1 + sdk/tdferrors.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index 3f6b0f59b4..91791470f9 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -3148,6 +3148,7 @@ func TestGetKasErrorToReturn(t *testing.T) { inputError := errors.New("rpc error: code = PermissionDenied desc = access denied") result := getKasErrorToReturn(inputError, defaultError) require.ErrorIs(t, result, ErrRewrapForbidden) + require.ErrorIs(t, result, ErrKASRequestError) require.ErrorIs(t, result, defaultError) }) diff --git a/sdk/tdferrors.go b/sdk/tdferrors.go index 8bf1a996f2..86d3f340ed 100644 --- a/sdk/tdferrors.go +++ b/sdk/tdferrors.go @@ -23,7 +23,7 @@ var ( // KAS request errors — client/configuration issues, not integrity failures ErrKASRequestError = errors.New("tdf: KAS request error") ErrRewrapBadRequest = fmt.Errorf("[%w] tdf: rewrap request 400", ErrKASRequestError) - ErrRewrapForbidden = errors.New("tdf: rewrap request 403") + ErrRewrapForbidden = fmt.Errorf("[%w] tdf: rewrap request 403", ErrKASRequestError) ) // Custom error struct for Assertion errors From 3464e5de05a513809c76ddf030ae4d201e924285 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 18 Mar 2026 10:15:46 -0700 Subject: [PATCH 03/12] fix(sdk): distinguish policy binding tamper from KAS misconfiguration KAS now returns FailedPrecondition (instead of InvalidArgument) for policy binding HMAC mismatches. The SDK maps this to a new ErrPolicyBindingFailure sentinel under ErrTampered, preserving tamper detection for real integrity failures while keeping misconfiguration errors (InvalidArgument) under ErrKASRequestError. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- sdk/tdf.go | 9 +++++++-- sdk/tdf_test.go | 9 +++++++++ sdk/tdferrors.go | 1 + service/kas/access/rewrap.go | 8 ++++++-- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/sdk/tdf.go b/sdk/tdf.go index dbfdedabf3..86cbb5be7c 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -1570,9 +1570,14 @@ func createKaoTemplateFromKasInfo(kasInfoArr []KASInfo) []kaoTpl { func getKasErrorToReturn(err error, defaultError error) error { errToReturn := defaultError - if strings.Contains(err.Error(), codes.InvalidArgument.String()) { + errStr := err.Error() + + switch { + case strings.Contains(errStr, codes.FailedPrecondition.String()): + errToReturn = errors.Join(ErrPolicyBindingFailure, errToReturn) + case strings.Contains(errStr, codes.InvalidArgument.String()): errToReturn = errors.Join(ErrRewrapBadRequest, errToReturn) - } else if strings.Contains(err.Error(), codes.PermissionDenied.String()) { + case strings.Contains(errStr, codes.PermissionDenied.String()): errToReturn = errors.Join(ErrRewrapForbidden, errToReturn) } diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index 91791470f9..d3fb8d8925 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -3135,6 +3135,15 @@ func TestIsLessThanSemver(t *testing.T) { func TestGetKasErrorToReturn(t *testing.T) { defaultError := errors.New("default KAS error") + t.Run("FailedPrecondition error returns ErrPolicyBindingFailure", func(t *testing.T) { + inputError := errors.New("rpc error: code = FailedPrecondition desc = policy binding mismatch") + result := getKasErrorToReturn(inputError, defaultError) + require.ErrorIs(t, result, ErrPolicyBindingFailure) + require.ErrorIs(t, result, ErrTampered, "policy binding failure must match ErrTampered") + require.NotErrorIs(t, result, ErrKASRequestError, "policy binding failure must not match ErrKASRequestError") + require.ErrorIs(t, result, defaultError) + }) + t.Run("InvalidArgument error returns ErrRewrapBadRequest", func(t *testing.T) { inputError := errors.New("rpc error: code = InvalidArgument desc = invalid request") result := getKasErrorToReturn(inputError, defaultError) diff --git a/sdk/tdferrors.go b/sdk/tdferrors.go index 86d3f340ed..b652f47db6 100644 --- a/sdk/tdferrors.go +++ b/sdk/tdferrors.go @@ -19,6 +19,7 @@ var ( ErrTDFPayloadReadFail = fmt.Errorf("[%w] tdf: fail to read payload from tdf", ErrTampered) ErrTDFPayloadInvalidOffset = fmt.Errorf("[%w] sdk.Reader.ReadAt: negative offset", ErrTampered) ErrRootSignatureFailure = fmt.Errorf("[%w] tdf: issue verifying root signature", ErrTampered) + ErrPolicyBindingFailure = fmt.Errorf("[%w] tdf: policy binding verification failed", ErrTampered) // KAS request errors — client/configuration issues, not integrity failures ErrKASRequestError = errors.New("tdf: KAS request error") diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index 274e09e256..5b78524d28 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -113,6 +113,10 @@ func err403(s string) error { return connect.NewError(connect.CodePermissionDenied, errors.Join(ErrUser, status.Error(codes.PermissionDenied, s))) } +func errPolicyBindingFailed(s string) error { + return connect.NewError(connect.CodeFailedPrecondition, errors.Join(ErrUser, status.Error(codes.FailedPrecondition, s))) +} + func err500(s string) error { return connect.NewError(connect.CodeInternal, errors.Join(ErrInternal, status.Error(codes.Internal, s))) } @@ -432,7 +436,7 @@ func verifyPolicyBinding(ctx context.Context, policy []byte, kao *kaspb.Unsigned if !hmac.Equal(actualHMAC, expectedHMAC) { //nolint:sloglint // usage of camelCase is intentional logger.WarnContext(ctx, "policy hmac mismatch", slog.String("policyBinding", policyBinding)) - return err400("bad request") + return errPolicyBindingFailed("policy binding mismatch") } return nil @@ -800,7 +804,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned // Verify policy binding using the UnwrappedKeyData interface if err := dek.VerifyBinding(ctx, []byte(req.GetPolicy().GetBody()), policyBinding); err != nil { p.Logger.WarnContext(ctx, "failure to verify policy binding", slog.Any("error", err)) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, errPolicyBindingFailed("policy binding mismatch")) continue } From a3dbdae1e4424a8618015ae65a0b0d63fe671a99 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 18 Mar 2026 10:23:41 -0700 Subject: [PATCH 04/12] Revert "fix(sdk): distinguish policy binding tamper from KAS misconfiguration" This reverts commit 33d7e9247a76d24987b223d9e8aa6f8a5c708c28. --- sdk/tdf.go | 9 ++------- sdk/tdf_test.go | 9 --------- sdk/tdferrors.go | 1 - service/kas/access/rewrap.go | 8 ++------ 4 files changed, 4 insertions(+), 23 deletions(-) diff --git a/sdk/tdf.go b/sdk/tdf.go index 86cbb5be7c..dbfdedabf3 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -1570,14 +1570,9 @@ func createKaoTemplateFromKasInfo(kasInfoArr []KASInfo) []kaoTpl { func getKasErrorToReturn(err error, defaultError error) error { errToReturn := defaultError - errStr := err.Error() - - switch { - case strings.Contains(errStr, codes.FailedPrecondition.String()): - errToReturn = errors.Join(ErrPolicyBindingFailure, errToReturn) - case strings.Contains(errStr, codes.InvalidArgument.String()): + if strings.Contains(err.Error(), codes.InvalidArgument.String()) { errToReturn = errors.Join(ErrRewrapBadRequest, errToReturn) - case strings.Contains(errStr, codes.PermissionDenied.String()): + } else if strings.Contains(err.Error(), codes.PermissionDenied.String()) { errToReturn = errors.Join(ErrRewrapForbidden, errToReturn) } diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index d3fb8d8925..91791470f9 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -3135,15 +3135,6 @@ func TestIsLessThanSemver(t *testing.T) { func TestGetKasErrorToReturn(t *testing.T) { defaultError := errors.New("default KAS error") - t.Run("FailedPrecondition error returns ErrPolicyBindingFailure", func(t *testing.T) { - inputError := errors.New("rpc error: code = FailedPrecondition desc = policy binding mismatch") - result := getKasErrorToReturn(inputError, defaultError) - require.ErrorIs(t, result, ErrPolicyBindingFailure) - require.ErrorIs(t, result, ErrTampered, "policy binding failure must match ErrTampered") - require.NotErrorIs(t, result, ErrKASRequestError, "policy binding failure must not match ErrKASRequestError") - require.ErrorIs(t, result, defaultError) - }) - t.Run("InvalidArgument error returns ErrRewrapBadRequest", func(t *testing.T) { inputError := errors.New("rpc error: code = InvalidArgument desc = invalid request") result := getKasErrorToReturn(inputError, defaultError) diff --git a/sdk/tdferrors.go b/sdk/tdferrors.go index b652f47db6..86d3f340ed 100644 --- a/sdk/tdferrors.go +++ b/sdk/tdferrors.go @@ -19,7 +19,6 @@ var ( ErrTDFPayloadReadFail = fmt.Errorf("[%w] tdf: fail to read payload from tdf", ErrTampered) ErrTDFPayloadInvalidOffset = fmt.Errorf("[%w] sdk.Reader.ReadAt: negative offset", ErrTampered) ErrRootSignatureFailure = fmt.Errorf("[%w] tdf: issue verifying root signature", ErrTampered) - ErrPolicyBindingFailure = fmt.Errorf("[%w] tdf: policy binding verification failed", ErrTampered) // KAS request errors — client/configuration issues, not integrity failures ErrKASRequestError = errors.New("tdf: KAS request error") diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index 5b78524d28..274e09e256 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -113,10 +113,6 @@ func err403(s string) error { return connect.NewError(connect.CodePermissionDenied, errors.Join(ErrUser, status.Error(codes.PermissionDenied, s))) } -func errPolicyBindingFailed(s string) error { - return connect.NewError(connect.CodeFailedPrecondition, errors.Join(ErrUser, status.Error(codes.FailedPrecondition, s))) -} - func err500(s string) error { return connect.NewError(connect.CodeInternal, errors.Join(ErrInternal, status.Error(codes.Internal, s))) } @@ -436,7 +432,7 @@ func verifyPolicyBinding(ctx context.Context, policy []byte, kao *kaspb.Unsigned if !hmac.Equal(actualHMAC, expectedHMAC) { //nolint:sloglint // usage of camelCase is intentional logger.WarnContext(ctx, "policy hmac mismatch", slog.String("policyBinding", policyBinding)) - return errPolicyBindingFailed("policy binding mismatch") + return err400("bad request") } return nil @@ -804,7 +800,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned // Verify policy binding using the UnwrappedKeyData interface if err := dek.VerifyBinding(ctx, []byte(req.GetPolicy().GetBody()), policyBinding); err != nil { p.Logger.WarnContext(ctx, "failure to verify policy binding", slog.Any("error", err)) - failedKAORewrap(results, kao, errPolicyBindingFailed("policy binding mismatch")) + failedKAORewrap(results, kao, err400("bad request")) continue } From bdd77f09d61ba86369b6d625301b7dce4505d80e Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 18 Mar 2026 10:28:19 -0700 Subject: [PATCH 05/12] fix(sdk): use descriptive KAS errors for misconfiguration, keep generic for tamper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of changing the gRPC status code for policy binding failures (which would leak information about secret-key computations), make non-secret KAS 400 errors descriptive (e.g. "unsupported key type", "key access object is nil") so the SDK can distinguish them from the generic "bad request" that policy binding failures produce. SDK logic: generic "bad request" from KAS → ErrRewrapBadRequest (under ErrTampered, potential integrity failure); specific message → ErrKASRequestError (misconfiguration, not tamper). Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- sdk/tdf.go | 15 ++++++++++++--- sdk/tdf_test.go | 14 +++++++++++--- sdk/tdferrors.go | 6 +++--- service/kas/access/rewrap.go | 26 +++++++++++++------------- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/sdk/tdf.go b/sdk/tdf.go index dbfdedabf3..720119527c 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -1570,9 +1570,18 @@ func createKaoTemplateFromKasInfo(kasInfoArr []KASInfo) []kaoTpl { func getKasErrorToReturn(err error, defaultError error) error { errToReturn := defaultError - if strings.Contains(err.Error(), codes.InvalidArgument.String()) { - errToReturn = errors.Join(ErrRewrapBadRequest, errToReturn) - } else if strings.Contains(err.Error(), codes.PermissionDenied.String()) { + errStr := err.Error() + + switch { + case strings.Contains(errStr, codes.InvalidArgument.String()): + // Generic "bad request" from KAS may indicate policy binding tamper; + // specific messages indicate client/configuration errors. + if strings.Contains(errStr, "bad request") { + errToReturn = errors.Join(ErrRewrapBadRequest, errToReturn) + } else { + errToReturn = errors.Join(ErrKASRequestError, errToReturn) + } + case strings.Contains(errStr, codes.PermissionDenied.String()): errToReturn = errors.Join(ErrRewrapForbidden, errToReturn) } diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index 91791470f9..268c6d5905 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -3135,13 +3135,21 @@ func TestIsLessThanSemver(t *testing.T) { func TestGetKasErrorToReturn(t *testing.T) { defaultError := errors.New("default KAS error") - t.Run("InvalidArgument error returns ErrRewrapBadRequest", func(t *testing.T) { - inputError := errors.New("rpc error: code = InvalidArgument desc = invalid request") + t.Run("generic InvalidArgument (bad request) is potential tamper", func(t *testing.T) { + inputError := errors.New("rpc error: code = InvalidArgument desc = bad request") result := getKasErrorToReturn(inputError, defaultError) require.ErrorIs(t, result, ErrRewrapBadRequest) + require.ErrorIs(t, result, ErrTampered, "generic bad request may be policy binding tamper") + require.NotErrorIs(t, result, ErrKASRequestError) + require.ErrorIs(t, result, defaultError) + }) + + t.Run("specific InvalidArgument is misconfiguration", func(t *testing.T) { + inputError := errors.New("rpc error: code = InvalidArgument desc = unsupported key type") + result := getKasErrorToReturn(inputError, defaultError) require.ErrorIs(t, result, ErrKASRequestError) + require.NotErrorIs(t, result, ErrTampered, "specific KAS 400 must not match ErrTampered") require.ErrorIs(t, result, defaultError) - require.NotErrorIs(t, result, ErrTampered, "KAS 400 must not match ErrTampered") }) t.Run("PermissionDenied error returns ErrRewrapForbidden", func(t *testing.T) { diff --git a/sdk/tdferrors.go b/sdk/tdferrors.go index 86d3f340ed..6be1396955 100644 --- a/sdk/tdferrors.go +++ b/sdk/tdferrors.go @@ -19,11 +19,11 @@ var ( ErrTDFPayloadReadFail = fmt.Errorf("[%w] tdf: fail to read payload from tdf", ErrTampered) ErrTDFPayloadInvalidOffset = fmt.Errorf("[%w] sdk.Reader.ReadAt: negative offset", ErrTampered) ErrRootSignatureFailure = fmt.Errorf("[%w] tdf: issue verifying root signature", ErrTampered) + ErrRewrapBadRequest = fmt.Errorf("[%w] tdf: rewrap request 400", ErrTampered) // KAS request errors — client/configuration issues, not integrity failures - ErrKASRequestError = errors.New("tdf: KAS request error") - ErrRewrapBadRequest = fmt.Errorf("[%w] tdf: rewrap request 400", ErrKASRequestError) - ErrRewrapForbidden = fmt.Errorf("[%w] tdf: rewrap request 403", ErrKASRequestError) + ErrKASRequestError = errors.New("tdf: KAS request error") + ErrRewrapForbidden = fmt.Errorf("[%w] tdf: rewrap request 403", ErrKASRequestError) ) // Custom error struct for Assertion errors diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index 274e09e256..7bb0a18f86 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -633,14 +633,14 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned for _, kao := range req.GetKeyAccessObjects() { if policyErr != nil { - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("invalid policy")) continue } // Check if KeyAccessObject is nil if kao.GetKeyAccessObject() == nil { p.Logger.WarnContext(ctx, "key access object is nil", slog.String("kao_id", kao.GetKeyAccessObjectId())) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("key access object is nil")) continue } @@ -648,7 +648,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned wrappedKey := kao.GetKeyAccessObject().GetWrappedKey() if len(wrappedKey) == 0 { p.Logger.WarnContext(ctx, "wrapped key is empty", slog.String("kao_id", kao.GetKeyAccessObjectId())) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("wrapped key is empty")) continue } @@ -659,7 +659,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned if !p.ECTDFEnabled && !p.Preview.ECTDFEnabled { p.Logger.WarnContext(ctx, "ec-wrapped not enabled") - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("ec-wrapped not enabled")) continue } @@ -674,7 +674,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned slog.Any("kao", kao), slog.Any("error", err), ) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("invalid ephemeral public key")) continue } @@ -685,7 +685,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned slog.Any("kao", kao), slog.Any("error", err), ) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("unsupported EC key size")) continue } @@ -697,7 +697,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned slog.Any("kao", kao), slog.Any("error", err), ) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("invalid ephemeral public key PEM")) continue } @@ -708,14 +708,14 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned slog.Any("kao", kao), slog.Any("error", err), ) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("invalid ephemeral public key")) continue } ecPub, ok := pub.(*ecdsa.PublicKey) if !ok { p.Logger.WarnContext(ctx, "not an EC public key", slog.Any("error", err)) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("ephemeral key is not EC")) continue } @@ -723,7 +723,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned compressedKey, err := ocrypto.CompressedECPublicKey(mode, *ecPub) if err != nil { p.Logger.WarnContext(ctx, "failed to compress public key", slog.Any("error", err)) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("invalid EC public key")) continue } @@ -743,7 +743,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned kidsToCheck = p.listLegacyKeys(ctx) if len(kidsToCheck) == 0 { p.Logger.WarnContext(ctx, "failure to find legacy kids for rsa") - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("no legacy key IDs found")) continue } } @@ -762,7 +762,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned p.Logger.WarnContext(ctx, "unsupported key type", slog.String("key_type", keyType), slog.String("kao_id", kao.GetKeyAccessObjectId())) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("unsupported key type")) continue } if err != nil { @@ -784,7 +784,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned n, err := base64.StdEncoding.Decode(policyBinding, []byte(policyBindingB64Encoded)) if err != nil { p.Logger.WarnContext(ctx, "invalid policy binding encoding", slog.Any("error", err)) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("invalid policy binding encoding")) continue } if n == 64 { //nolint:mnd // 32 bytes of hex encoded data = 256 bit sha-2 From 3dd9479ce64a2f73022f9a74e351b67c625dbe25 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 18 Mar 2026 10:32:25 -0700 Subject: [PATCH 06/12] chore(docs): add security comments explaining generic vs descriptive KAS errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document why policy binding and DEK decryption failures intentionally use generic "bad request" messages — to avoid leaking information about computations involving secret key material. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- sdk/tdf.go | 4 ++++ service/kas/access/rewrap.go | 13 ++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/sdk/tdf.go b/sdk/tdf.go index 720119527c..a14a78de70 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -1568,6 +1568,10 @@ func createKaoTemplateFromKasInfo(kasInfoArr []KASInfo) []kaoTpl { return kaoTemplate } +// getKasErrorToReturn classifies KAS rewrap errors. KAS intentionally uses a +// generic "bad request" for policy binding and DEK failures to avoid leaking +// information about secret key material. Descriptive messages indicate +// client/configuration issues that are safe to surface as non-tamper errors. func getKasErrorToReturn(err error, defaultError error) error { errToReturn := defaultError errStr := err.Error() diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index 7bb0a18f86..82670efdd8 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -101,6 +101,13 @@ const ( errNoValidKeyAccessObjects = Error("no valid KAOs") ) +// Error helpers for KAS rewrap responses. +// +// SECURITY: Policy binding verification and DEK decryption failures MUST use the +// generic "bad request" message to avoid leaking information about computations +// involving secret key material. Non-secret failures (malformed input, unsupported +// key types, missing fields) SHOULD use descriptive messages so the SDK can +// distinguish misconfiguration from potential tamper. func err400(s string) error { return connect.NewError(connect.CodeInvalidArgument, errors.Join(ErrUser, status.Error(codes.InvalidArgument, s))) } @@ -432,7 +439,7 @@ func verifyPolicyBinding(ctx context.Context, policy []byte, kao *kaspb.Unsigned if !hmac.Equal(actualHMAC, expectedHMAC) { //nolint:sloglint // usage of camelCase is intentional logger.WarnContext(ctx, "policy hmac mismatch", slog.String("policyBinding", policyBinding)) - return err400("bad request") + return err400("bad request") // Generic: involves secret key material } return nil @@ -767,7 +774,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned } if err != nil { p.Logger.WarnContext(ctx, "failure to decrypt dek", slog.Any("error", err)) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("bad request")) // Generic: involves secret key material continue } @@ -800,7 +807,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned // Verify policy binding using the UnwrappedKeyData interface if err := dek.VerifyBinding(ctx, []byte(req.GetPolicy().GetBody()), policyBinding); err != nil { p.Logger.WarnContext(ctx, "failure to verify policy binding", slog.Any("error", err)) - failedKAORewrap(results, kao, err400("bad request")) + failedKAORewrap(results, kao, err400("bad request")) // Generic: involves secret key material continue } From 357108273e1a32a3091077e35533e5cadd5e6dfb Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 18 Mar 2026 10:48:30 -0700 Subject: [PATCH 07/12] fix(kas): keep generic error for corrupted policy and malformed binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Policy body parse failures and binding encoding errors may indicate tamper — keep them as generic "bad request" so the SDK classifies them under ErrTampered. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- service/kas/access/rewrap.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index 82670efdd8..ea704232a1 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -640,7 +640,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned for _, kao := range req.GetKeyAccessObjects() { if policyErr != nil { - failedKAORewrap(results, kao, err400("invalid policy")) + failedKAORewrap(results, kao, err400("bad request")) // Generic: corrupted policy body may indicate tamper continue } @@ -791,7 +791,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned n, err := base64.StdEncoding.Decode(policyBinding, []byte(policyBindingB64Encoded)) if err != nil { p.Logger.WarnContext(ctx, "invalid policy binding encoding", slog.Any("error", err)) - failedKAORewrap(results, kao, err400("invalid policy binding encoding")) + failedKAORewrap(results, kao, err400("bad request")) // Generic: malformed binding may indicate tamper continue } if n == 64 { //nolint:mnd // 32 bytes of hex encoded data = 256 bit sha-2 From 3a40427d2bf2939dad2638b47ba85b85786ee5b9 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 18 Mar 2026 10:51:31 -0700 Subject: [PATCH 08/12] chore(sdk): add shared constant for generic KAS error and expand tests Extract "bad request" into kasGenericBadRequest constant in the SDK so both sides of the contract are linked. Add KAS-side comment referencing the SDK constant. Expand test coverage with subtests for all descriptive KAS messages and a substring false-match edge case. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- sdk/tdf.go | 2 +- sdk/tdf_test.go | 27 +++++++++++++++++++++++---- sdk/tdferrors.go | 6 ++++++ service/kas/access/rewrap.go | 4 ++++ 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/sdk/tdf.go b/sdk/tdf.go index a14a78de70..929d6c28d3 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -1580,7 +1580,7 @@ func getKasErrorToReturn(err error, defaultError error) error { case strings.Contains(errStr, codes.InvalidArgument.String()): // Generic "bad request" from KAS may indicate policy binding tamper; // specific messages indicate client/configuration errors. - if strings.Contains(errStr, "bad request") { + if strings.Contains(errStr, kasGenericBadRequest) { errToReturn = errors.Join(ErrRewrapBadRequest, errToReturn) } else { errToReturn = errors.Join(ErrKASRequestError, errToReturn) diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index 268c6d5905..fd3edb8994 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -3145,11 +3145,30 @@ func TestGetKasErrorToReturn(t *testing.T) { }) t.Run("specific InvalidArgument is misconfiguration", func(t *testing.T) { - inputError := errors.New("rpc error: code = InvalidArgument desc = unsupported key type") + for _, msg := range []string{ + "unsupported key type", + "key access object is nil", + "ec-wrapped not enabled", + "wrapped key is empty", + "invalid ephemeral public key", + } { + t.Run(msg, func(t *testing.T) { + inputError := errors.New("rpc error: code = InvalidArgument desc = " + msg) + result := getKasErrorToReturn(inputError, defaultError) + require.ErrorIs(t, result, ErrKASRequestError) + require.NotErrorIs(t, result, ErrTampered, "specific KAS 400 must not match ErrTampered") + require.ErrorIs(t, result, defaultError) + }) + } + }) + + t.Run("message containing bad request as substring is still tamper", func(t *testing.T) { + // A message like "bad request body" contains "bad request" as a substring, + // so it should be classified as potential tamper (conservative approach). + inputError := errors.New("rpc error: code = InvalidArgument desc = bad request body") result := getKasErrorToReturn(inputError, defaultError) - require.ErrorIs(t, result, ErrKASRequestError) - require.NotErrorIs(t, result, ErrTampered, "specific KAS 400 must not match ErrTampered") - require.ErrorIs(t, result, defaultError) + require.ErrorIs(t, result, ErrRewrapBadRequest) + require.ErrorIs(t, result, ErrTampered, "substring match should err on side of tamper") }) t.Run("PermissionDenied error returns ErrRewrapForbidden", func(t *testing.T) { diff --git a/sdk/tdferrors.go b/sdk/tdferrors.go index 6be1396955..ab7a357d6e 100644 --- a/sdk/tdferrors.go +++ b/sdk/tdferrors.go @@ -21,6 +21,12 @@ var ( ErrRootSignatureFailure = fmt.Errorf("[%w] tdf: issue verifying root signature", ErrTampered) ErrRewrapBadRequest = fmt.Errorf("[%w] tdf: rewrap request 400", ErrTampered) + // kasGenericBadRequest is the exact message KAS uses for errors involving + // secret key material (policy binding, DEK decryption). If this string + // appears in a KAS 400 error, the SDK treats it as potential tamper. + // Must match the "bad request" message in service/kas/access/rewrap.go. + kasGenericBadRequest = "bad request" + // KAS request errors — client/configuration issues, not integrity failures ErrKASRequestError = errors.New("tdf: KAS request error") ErrRewrapForbidden = fmt.Errorf("[%w] tdf: rewrap request 403", ErrKASRequestError) diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index ea704232a1..633c0d539f 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -108,6 +108,10 @@ const ( // involving secret key material. Non-secret failures (malformed input, unsupported // key types, missing fields) SHOULD use descriptive messages so the SDK can // distinguish misconfiguration from potential tamper. +// +// The SDK matches on the exact string "bad request" to identify potential tamper +// (see sdk/tdferrors.go kasGenericBadRequest). Do not change this message without +// updating the SDK constant. func err400(s string) error { return connect.NewError(connect.CodeInvalidArgument, errors.Join(ErrUser, status.Error(codes.InvalidArgument, s))) } From 2b15967627cd1dfce2b39e3620d2306da0633bfe Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 18 Mar 2026 11:05:22 -0700 Subject: [PATCH 09/12] docs(docs): add instructions for running xtests against feature branches Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- docs/Contributing.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/Contributing.md b/docs/Contributing.md index 45dbc59519..a3e0f1447e 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -86,6 +86,23 @@ go install github.com/sudorandom/protoc-gen-connect-openapi@latest Make sure your Go bin directory (usually `$HOME/go/bin`) is in your `PATH`. +## Cross-Platform Integration Testing (xtests) + +The [opentdf/tests](https://github.com/opentdf/tests) repo contains cross-SDK compatibility tests that validate platform changes against Go, Java, and JavaScript SDKs. Xtests run automatically on every platform PR via the `platform-xtest` CI job. + +### Running xtests against a platform feature branch + +To manually trigger xtests against your platform changes before merging: + +1. Go to the **Actions** tab in [opentdf/tests](https://github.com/opentdf/tests/actions) +2. Find the **xtest** workflow +3. Click the **"Run workflow"** dropdown on the right +4. Set **"Use workflow from"** to the xtest branch to run (`main`, or a companion xtest branch if you have test changes) +5. Set **"platform ref branch"** to the HEAD commit SHA of your platform feature branch +6. Click **Run workflow** + +This is especially useful when your platform changes affect SDK error handling, KAS behavior, or the rewrap flow — areas where cross-SDK compatibility matters. + ## Advice for Code Contributors * Make sure to run our linters with `make lint` From 311f2db7bd319901eab2cbb202341528afe84671 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 25 Mar 2026 09:31:53 -0700 Subject: [PATCH 10/12] fix(sdk): anchor tamper detection to gRPC desc prefix and harden error messages Address code review feedback: - Change kasGenericBadRequest from "bad request" to "desc = bad request" to anchor the match to the gRPC status description field, avoiding false positives from middleware or error wrapping - Replace dynamic err400(err.Error()) with fixed descriptive message to prevent information leakage and accidental tamper classification - Add all 10 descriptive KAS messages to the test table (was 5) - Add test for middleware-injected "bad request" without desc prefix - Update comments to accurately describe substring matching behavior Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- sdk/tdf.go | 6 ++++-- sdk/tdf_test.go | 17 ++++++++++++++++- sdk/tdferrors.go | 20 +++++++++++++++----- service/kas/access/rewrap.go | 9 +++++---- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/sdk/tdf.go b/sdk/tdf.go index 929d6c28d3..db22208b5f 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -1578,8 +1578,10 @@ func getKasErrorToReturn(err error, defaultError error) error { switch { case strings.Contains(errStr, codes.InvalidArgument.String()): - // Generic "bad request" from KAS may indicate policy binding tamper; - // specific messages indicate client/configuration errors. + // Per-KAO errors are serialized as plain strings through the proto + // response, so we match on a substring anchored to the gRPC status + // description. Generic "bad request" = potential tamper; anything + // else = client/configuration error. if strings.Contains(errStr, kasGenericBadRequest) { errToReturn = errors.Join(ErrRewrapBadRequest, errToReturn) } else { diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index fd3edb8994..e4ef9cdacb 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -3151,6 +3151,12 @@ func TestGetKasErrorToReturn(t *testing.T) { "ec-wrapped not enabled", "wrapped key is empty", "invalid ephemeral public key", + "unsupported EC key size", + "invalid ephemeral public key PEM", + "ephemeral key is not EC", + "invalid EC public key", + "no legacy key IDs found", + "failed to get additional rewrap context", } { t.Run(msg, func(t *testing.T) { inputError := errors.New("rpc error: code = InvalidArgument desc = " + msg) @@ -3163,7 +3169,7 @@ func TestGetKasErrorToReturn(t *testing.T) { }) t.Run("message containing bad request as substring is still tamper", func(t *testing.T) { - // A message like "bad request body" contains "bad request" as a substring, + // "desc = bad request body" contains "desc = bad request" as a substring, // so it should be classified as potential tamper (conservative approach). inputError := errors.New("rpc error: code = InvalidArgument desc = bad request body") result := getKasErrorToReturn(inputError, defaultError) @@ -3171,6 +3177,15 @@ func TestGetKasErrorToReturn(t *testing.T) { require.ErrorIs(t, result, ErrTampered, "substring match should err on side of tamper") }) + t.Run("middleware injecting bad request without desc prefix is not tamper", func(t *testing.T) { + // A message like "bad request: unsupported key type" without the "desc = " + // prefix should NOT trigger tamper classification. + inputError := errors.New("rpc error: code = InvalidArgument bad request: unsupported key type") + result := getKasErrorToReturn(inputError, defaultError) + require.ErrorIs(t, result, ErrKASRequestError) + require.NotErrorIs(t, result, ErrTampered, "unanchored bad request should not match") + }) + t.Run("PermissionDenied error returns ErrRewrapForbidden", func(t *testing.T) { inputError := errors.New("rpc error: code = PermissionDenied desc = access denied") result := getKasErrorToReturn(inputError, defaultError) diff --git a/sdk/tdferrors.go b/sdk/tdferrors.go index ab7a357d6e..a178547963 100644 --- a/sdk/tdferrors.go +++ b/sdk/tdferrors.go @@ -21,11 +21,21 @@ var ( ErrRootSignatureFailure = fmt.Errorf("[%w] tdf: issue verifying root signature", ErrTampered) ErrRewrapBadRequest = fmt.Errorf("[%w] tdf: rewrap request 400", ErrTampered) - // kasGenericBadRequest is the exact message KAS uses for errors involving - // secret key material (policy binding, DEK decryption). If this string - // appears in a KAS 400 error, the SDK treats it as potential tamper. - // Must match the "bad request" message in service/kas/access/rewrap.go. - kasGenericBadRequest = "bad request" + // kasGenericBadRequest is the substring the SDK looks for in serialized + // KAS 400 errors to identify potential tamper. KAS uses the generic message + // "bad request" for errors involving secret key material (policy binding, + // DEK decryption). Per-KAO errors are serialized as plain strings through + // the proto response (not as gRPC status errors), so substring matching is + // the only classification mechanism available. + // + // The "desc = " prefix anchors the match to the gRPC status description + // field, avoiding false positives from middleware or error wrapping that + // might incidentally contain "bad request". + // + // Must stay in sync with the "bad request" message in + // service/kas/access/rewrap.go — and descriptive KAS messages must NOT + // contain this substring. + kasGenericBadRequest = "desc = bad request" // KAS request errors — client/configuration issues, not integrity failures ErrKASRequestError = errors.New("tdf: KAS request error") diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index 633c0d539f..85ba61be0c 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -109,9 +109,10 @@ const ( // key types, missing fields) SHOULD use descriptive messages so the SDK can // distinguish misconfiguration from potential tamper. // -// The SDK matches on the exact string "bad request" to identify potential tamper -// (see sdk/tdferrors.go kasGenericBadRequest). Do not change this message without -// updating the SDK constant. +// The SDK matches on the substring "desc = bad request" in serialized per-KAO +// errors to identify potential tamper (see sdk/tdferrors.go kasGenericBadRequest). +// Do not change the generic "bad request" message without updating the SDK +// constant, and do not use "bad request" in descriptive error messages. func err400(s string) error { return connect.NewError(connect.CodeInvalidArgument, errors.Join(ErrUser, status.Error(codes.InvalidArgument, s))) } @@ -577,7 +578,7 @@ func (p *Provider) Rewrap(ctx context.Context, req *connect.Request[kaspb.Rewrap additionalRewrapContext, err := getAdditionalRewrapContext(req.Header()) if err != nil { p.Logger.WarnContext(ctx, "failed to get additional rewrap context", slog.Any("error", err)) - return nil, err400(err.Error()) + return nil, err400("failed to get additional rewrap context") } resp.SessionPublicKey, results, err = p.tdf3Rewrap(ctx, tdf3Reqs, body.GetClientPublicKey(), entityInfo, additionalRewrapContext) if err != nil { From 48214c23cdbe80d4c8a475b3968704c8f575d981 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 25 Mar 2026 11:03:32 -0700 Subject: [PATCH 11/12] fix(kas): preserve per-KAO tamper signals on policy decode failure When verifyRewrapRequests returned policyErr, tdf3Rewrap discarded the per-KAO results (which contained generic "bad request" tamper signals) and returned err400("invalid request") instead. The SDK classified "invalid request" as ErrKASRequestError, silently losing the tamper signal for corrupted policy bodies. Fix: always store per-KAO results before continuing, regardless of the error type from verifyRewrapRequests. This ensures the SDK receives the per-KAO "bad request" entries and classifies them as ErrTampered. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- service/kas/access/rewrap.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index 85ba61be0c..d62975a480 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -878,12 +878,12 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, requests []*kaspb.UnsignedRew continue } policy, kaoResults, err := p.verifyRewrapRequests(ctx, req) - if err != nil && !errors.Is(err, errNoValidKeyAccessObjects) { - return "", nil, err400("invalid request") - } policyID := req.GetPolicy().GetId() results[policyID] = kaoResults if err != nil { + // Store per-KAO results even on error so tamper signals (e.g. corrupted + // policy body → generic "bad request") reach the SDK rather than being + // replaced by a top-level "invalid request". p.Logger.WarnContext(ctx, "rewrap: verifyRewrapRequests failed", slog.String("policy_id", policyID), From 8ec818208086617b46fc22d72c5e9699c5a1e10d Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Thu, 26 Mar 2026 15:49:30 -0700 Subject: [PATCH 12/12] fix(sdk): un-nest ErrRewrapForbidden from ErrKASRequestError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 400 (misconfiguration) and a 403 (authorization denied) have different failure modes and different remediation paths. Nesting ErrRewrapForbidden under ErrKASRequestError repeats the same lumping problem this PR set out to fix — callers catching ErrKASRequestError to retry with different config would incorrectly retry on 403s too. The ticket (DSPX-2606) explicitly specified ErrKASRequestError should be "separate from ErrRewrapForbidden (authorization)." Restore that separation: ErrRewrapForbidden is now a standalone sentinel again. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- sdk/tdf_test.go | 2 +- sdk/tdferrors.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index e4ef9cdacb..0aa58daa3f 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -3190,7 +3190,7 @@ func TestGetKasErrorToReturn(t *testing.T) { inputError := errors.New("rpc error: code = PermissionDenied desc = access denied") result := getKasErrorToReturn(inputError, defaultError) require.ErrorIs(t, result, ErrRewrapForbidden) - require.ErrorIs(t, result, ErrKASRequestError) + require.NotErrorIs(t, result, ErrKASRequestError, "403 is authorization, not misconfiguration") require.ErrorIs(t, result, defaultError) }) diff --git a/sdk/tdferrors.go b/sdk/tdferrors.go index a178547963..37a69a23ac 100644 --- a/sdk/tdferrors.go +++ b/sdk/tdferrors.go @@ -39,7 +39,7 @@ var ( // KAS request errors — client/configuration issues, not integrity failures ErrKASRequestError = errors.New("tdf: KAS request error") - ErrRewrapForbidden = fmt.Errorf("[%w] tdf: rewrap request 403", ErrKASRequestError) + ErrRewrapForbidden = errors.New("tdf: rewrap request 403") ) // Custom error struct for Assertion errors