diff --git a/MODULE.bazel b/MODULE.bazel index 194926e..d67fa04 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -33,7 +33,7 @@ go_sdk.download(version = "1.26.4") go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps") go_deps.from_file(go_mod = "//:go.mod") -use_repo(go_deps, "com_github_coredhcp_coredhcp", "com_github_golang_glog", "com_github_google_go_cmp", "com_github_google_go_tpm", "com_github_insomniacslk_dhcp", "com_github_openconfig_monax", "org_golang_google_grpc", "org_golang_google_grpc_cmd_protoc_gen_go_grpc", "org_golang_google_protobuf", "org_golang_x_net", "org_mozilla_go_pkcs7") +use_repo(go_deps, "com_github_coredhcp_coredhcp", "com_github_golang_glog", "com_github_google_go_cmp", "com_github_google_go_tpm", "com_github_insomniacslk_dhcp", "com_github_jonboulle_clockwork", "com_github_openconfig_monax", "org_golang_google_grpc", "org_golang_google_grpc_cmd_protoc_gen_go_grpc", "org_golang_google_protobuf", "org_golang_x_net", "org_mozilla_go_pkcs7") go_deps_dev = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps", dev_dependency = True) diff --git a/go.mod b/go.mod index ce2fa94..9aa35fe 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/go-tpm v0.9.8 github.com/insomniacslk/dhcp v0.0.0-20260603135910-a415979eb11e + github.com/jonboulle/clockwork v0.5.0 github.com/openconfig/attestz v0.6.15 github.com/openconfig/gnsi v1.9.1 github.com/openconfig/monax v0.0.0-20260720151517-883500dc841b diff --git a/go.sum b/go.sum index 7f88f28..247a902 100644 --- a/go.sum +++ b/go.sum @@ -101,6 +101,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/insomniacslk/dhcp v0.0.0-20260603135910-a415979eb11e h1:7j1+lOuGBg7PQF1RxeVx0iP+/GpiAxSG/F+9t5JOS94= github.com/insomniacslk/dhcp v0.0.0-20260603135910-a415979eb11e/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/server/service/BUILD.bazel b/server/service/BUILD.bazel index d06990f..50bbcb8 100644 --- a/server/service/BUILD.bazel +++ b/server/service/BUILD.bazel @@ -27,6 +27,7 @@ go_library( "//proto:bootz", "@com_github_golang_glog//:glog", "@com_github_google_go_tpm//tpm2", + "@com_github_jonboulle_clockwork//:clockwork", "@openconfig_attestz//proto:tpm_enrollz_go", "@openconfig_attestz//service/biz:enrollz_biz", "@openconfig_attestz//service/biz:tpm20_utils", @@ -50,6 +51,7 @@ go_test( "//proto:bootz", "@com_github_google_go_cmp//cmp", "@com_github_google_go_tpm//tpm2", + "@com_github_jonboulle_clockwork//:clockwork", "@openconfig_attestz//proto:tpm_enrollz_go", "@openconfig_attestz//service/biz:enrollz_biz", "@openconfig_attestz//service/biz:tpm20_utils", diff --git a/server/service/service.go b/server/service/service.go index 27f312e..c550c37 100644 --- a/server/service/service.go +++ b/server/service/service.go @@ -34,9 +34,11 @@ import ( "net" "slices" "strings" + "time" log "github.com/golang/glog" "github.com/google/go-tpm/tpm2" + "github.com/jonboulle/clockwork" "github.com/openconfig/attestz/service/biz" ownercertificate "github.com/openconfig/bootz/common/owner_certificate" "github.com/openconfig/bootz/common/signature" @@ -62,6 +64,10 @@ const ( stateAttested ) +const ( + hmacChallengeTimeout = 10 * time.Second +) + // ArtifactManager is an interface for providing security artifacts to the Bootz service. These artifacts are // associated either with the Bootz server itself (e.g. the Bootz server trust anchor keypair), or with a specific // control card in a chassis (e.g. Ownership Vouchers, EK/PPK keys). @@ -95,27 +101,30 @@ type Service struct { am ArtifactManager cm ChassisManager tpm20 biz.TPM20Utils + clock clockwork.Clock } type streamSession struct { - stream bpb.Bootstrap_BootstrapStreamServer - currentState int - chassis *types.Chassis // Store chassis info for later stages - status *bpb.ReportStatusRequest // Store status for later stages - clientNonce string // client nonce from bootstrap request - serverNonce string // For TPM 2.0 with IDevID nonce challenge - hmacSensitive *tpm2.TPMTSensitive // For TPM 2.0 without IDevID HMAC challenge + stream bpb.Bootstrap_BootstrapStreamServer + currentState int + chassis *types.Chassis // Store chassis info for later stages + status *bpb.ReportStatusRequest // Store status for later stages + clientNonce string // client nonce from bootstrap request + serverNonce string // For TPM 2.0 with IDevID nonce challenge + hmacSensitive *tpm2.TPMTSensitive // For TPM 2.0 without IDevID HMAC challenge + challengeSentTime time.Time } type streamSessionV1 struct { - stream bpb.Bootstrap_BootstrapStreamV1Server - currentState int - chassis *types.Chassis // Store chassis info for later stages - status *bpb.ReportStatusRequest // Store status for later stages - clientNonce string // client nonce from bootstrap request - serverNonce []byte // For TPM 2.0 with IDevID nonce challenge - hmacSensitive *tpm2.TPMTSensitive // For TPM 2.0 without IDevID HMAC challenge - hmacKey []byte // For TPM 1.2 EK challenge + stream bpb.Bootstrap_BootstrapStreamV1Server + currentState int + chassis *types.Chassis // Store chassis info for later stages + status *bpb.ReportStatusRequest // Store status for later stages + clientNonce string // client nonce from bootstrap request + serverNonce []byte // For TPM 2.0 with IDevID nonce challenge + hmacSensitive *tpm2.TPMTSensitive // For TPM 2.0 without IDevID HMAC challenge + hmacKey []byte // For TPM 1.2 EK challenge + challengeSentTime time.Time } // TPMAsymCAContents is the TPM_ASYM_CA_CONTENTS structure defined in the TPM 1.2 specification. @@ -344,6 +353,10 @@ func (s *Service) BootstrapStream(stream bpb.Bootstrap_BootstrapStreamServer) er if session.hmacSensitive == nil { return status.Errorf(codes.FailedPrecondition, "received unexpected TPM 2.0 HMAC challenge response") } + if elapsed := s.clock.Since(session.challengeSentTime); elapsed > hmacChallengeTimeout { + log.Errorf("HMAC challenge timeout exceeded for device %s: took %v, limit %v", session.chassis.ActiveSerial, elapsed, hmacChallengeTimeout) + return status.Errorf(codes.DeadlineExceeded, "HMAC challenge timeout exceeded") + } hmacResponse := challengeResponse.GetHmacChallengeResponse() tpm2BAttest, err := tpm2.Unmarshal[tpm2.TPM2BAttest](hmacResponse.GetIakCertifyInfo()) @@ -537,6 +550,10 @@ func (s *Service) BootstrapStreamV1(stream bpb.Bootstrap_BootstrapStreamV1Server if session.hmacSensitive == nil { return status.Errorf(codes.FailedPrecondition, "received unexpected TPM20HMAC challenge response") } + if elapsed := s.clock.Since(session.challengeSentTime); elapsed > hmacChallengeTimeout { + log.Errorf("HMAC challenge timeout exceeded for device %s: took %v, limit %v", session.chassis.ActiveSerial, elapsed, hmacChallengeTimeout) + return status.Errorf(codes.DeadlineExceeded, "HMAC challenge timeout exceeded") + } // Verify HMAC challenge response. mac := challengeResponse.GetTpm20Hmac().GetHmac() tpm2BAttest, err := tpm2.Unmarshal[tpm2.TPM2BAttest](mac.GetIakCertifyInfo()) @@ -710,6 +727,9 @@ func (s *Service) BootstrapStreamV1(stream bpb.Bootstrap_BootstrapStreamV1Server return status.Errorf(codes.Internal, "failed to send BootstrapStreamResponseV1 message: %v", err) } log.Infof("Sent BootstrapStreamResponseV1 message to device %s", session.chassis.ActiveSerial) + if (session.currentState == stateChallengeSent || session.currentState == stateReauthChallengeSent) && session.hmacSensitive != nil { + session.challengeSentTime = s.clock.Now() + } } } @@ -1050,6 +1070,9 @@ func (s *Service) establishSessionAndSendChallenge(session *streamSession) error return err } log.Infof("Sent challenge to device %s", session.chassis.ActiveSerial) + if session.hmacSensitive != nil { + session.challengeSentTime = s.clock.Now() + } return nil } @@ -1150,5 +1173,6 @@ func New(am ArtifactManager, cm ChassisManager, tpm20 biz.TPM20Utils) (*Service, am: am, cm: cm, tpm20: tpm20, + clock: clockwork.NewRealClock(), }, nil } diff --git a/server/service/service_test.go b/server/service/service_test.go index bc2ef0f..6194b5d 100644 --- a/server/service/service_test.go +++ b/server/service/service_test.go @@ -31,9 +31,11 @@ import ( "math/big" "net" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/google/go-tpm/tpm2" + "github.com/jonboulle/clockwork" "github.com/openconfig/attestz/service/biz" ownercertificate "github.com/openconfig/bootz/common/owner_certificate" ownershipvoucher "github.com/openconfig/bootz/common/ownership_voucher" @@ -239,12 +241,13 @@ func TestBootstrapStream(t *testing.T) { idNoIdevid := &bpb.Identity{Type: &bpb.Identity_EkPpkPub{EkPpkPub: true}} tests := []struct { - name string - req *bpb.BootstrapStreamRequest - id *bpb.Identity - wantErrCode codes.Code - signedNonce []byte - reportStatus bool + name string + req *bpb.BootstrapStreamRequest + id *bpb.Identity + wantErrCode codes.Code + wantSecondRecvErrCode codes.Code + signedNonce []byte + reportStatus bool }{ { name: "Missing Identity - Invalid Argument", @@ -271,6 +274,12 @@ func TestBootstrapStream(t *testing.T) { req: statusReq, id: idNoIdevid, }, + { + name: "TPM 2.0 no-IDevID Flow Timeout - Failure", + req: initialReq, + id: idNoIdevid, + wantSecondRecvErrCode: codes.DeadlineExceeded, + }, } for _, test := range tests { @@ -279,6 +288,11 @@ func TestBootstrapStream(t *testing.T) { if err != nil { t.Fatalf("New() failed: %v", err) } + var fakeClock *clockwork.FakeClock + if test.name == "TPM 2.0 no-IDevID Flow Timeout - Failure" { + fakeClock = clockwork.NewFakeClock() + s.clock = fakeClock + } srv := grpc.NewServer(grpc.Creds(insecure.NewCredentials())) bpb.RegisterBootstrapServer(srv, s) addr := startTestServer(t, srv) @@ -371,12 +385,23 @@ func TestBootstrapStream(t *testing.T) { default: t.Fatalf("Unexpected challenge type %T", challengeType) } + if fakeClock != nil { + fakeClock.Advance(11 * time.Second) + } if err := stream.Send(responseReq); err != nil { t.Fatalf("stream.Send(responseReq) failed: %v", err) } // === Second Recv: Expect Bootstrap Data or Error === finalResp, err := stream.Recv() + if test.wantSecondRecvErrCode != codes.OK { + if err == nil { + t.Errorf("stream.Recv() got response %v, want error code %v", finalResp, test.wantSecondRecvErrCode) + } else if stat, ok := status.FromError(err); ok && stat.Code() != test.wantSecondRecvErrCode { + t.Errorf("stream.Recv() got error code %v, want %v: %v", stat.Code(), test.wantSecondRecvErrCode, err) + } + return + } if err != nil { t.Fatalf("stream.Recv() for final response got unexpected error: %v", err) } @@ -530,6 +555,12 @@ func TestBootstrapStreamV1(t *testing.T) { id: idIdevid, wantCodes: []codes.Code{codes.OK, codes.OK}, }, + { + name: "TPM 2.0 EK Flow Timeout - Failure", + req: initialReq, + id: idTPM20EK, + wantCodes: []codes.Code{codes.OK, codes.DeadlineExceeded}, + }, } for _, test := range tests { @@ -538,6 +569,11 @@ func TestBootstrapStreamV1(t *testing.T) { if err != nil { t.Fatalf("New() failed: %v", err) } + var fakeClock *clockwork.FakeClock + if test.name == "TPM 2.0 EK Flow Timeout - Failure" { + fakeClock = clockwork.NewFakeClock() + s.clock = fakeClock + } srv := grpc.NewServer(grpc.Creds(insecure.NewCredentials())) bpb.RegisterBootstrapServer(srv, s) addr := startTestServer(t, srv) @@ -643,6 +679,9 @@ func TestBootstrapStreamV1(t *testing.T) { }, } } + if fakeClock != nil { + fakeClock.Advance(11 * time.Second) + } case 2: // Send Report Status Request. request = proto.Clone(statusReq).(*bpb.BootstrapStreamRequestV1) }