From 96cb9614830192ae03b7d7ce792d0efe7e08b887 Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Wed, 26 Aug 2026 13:12:08 -0700 Subject: [PATCH 1/7] mint-dev --- .../internal/actoridentity/actoridentity.go | 36 ++- .../actoridentity/actoridentity_test.go | 134 ++++++++ .../controlapi/zz_generated.validation.go | 288 ++++++++++++++++++ pkg/proto/ateapipb/ateapi.pb.go | 41 ++- pkg/proto/ateapipb/ateapi.proto | 25 +- 5 files changed, 498 insertions(+), 26 deletions(-) diff --git a/cmd/ateapi/internal/actoridentity/actoridentity.go b/cmd/ateapi/internal/actoridentity/actoridentity.go index 1dfa5efb0f..8844b660f8 100644 --- a/cmd/ateapi/internal/actoridentity/actoridentity.go +++ b/cmd/ateapi/internal/actoridentity/actoridentity.go @@ -28,6 +28,7 @@ import ( "time" "github.com/agent-substrate/substrate/cmd/ateapi/internal/actoridjwt" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/controlapi" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" "github.com/agent-substrate/substrate/internal/localca" @@ -40,6 +41,7 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" + "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -94,6 +96,10 @@ func (s *Server) MintJWT(ctx context.Context, req *ateapipb.MintJWTRequest) (*at return nil, status.Errorf(codes.PermissionDenied, "caller is not permitted to mint actor JWTs") } + if errs := validateMintJWTRequest(ctx, req); len(errs) > 0 { + return nil, status.Error(codes.InvalidArgument, errs.ToAggregate().Error()) + } + // TODO: Cross-check the verified caller and requested actor against the actor database. // TODO: Cache signing keys in memory, so we don't read from disk every time. @@ -106,10 +112,6 @@ func (s *Server) MintJWT(ctx context.Context, req *ateapipb.MintJWTRequest) (*at if err != nil { return nil, fmt.Errorf("while unmarshaling signing pool: %w", err) } - // We only issue tokens with audience bindings. - if len(req.GetAudience()) == 0 { - return nil, fmt.Errorf("at least one audience must be requested") - } actorClaims := &actoridjwt.Claims{ // TODO: This is currently API but it has to be a globally unique, oidc-compliant and accsible DNS name @@ -150,16 +152,14 @@ func (s *Server) MintCert(ctx context.Context, req *ateapipb.MintCertRequest) (* if err != nil { return nil, err } + if errs := validateMintCertRequest(ctx, req); len(errs) > 0 { + return nil, status.Error(codes.InvalidArgument, errs.ToAggregate().Error()) + } + // Validation bounds purpose to the enum's range; which purposes this + // server actually supports is a policy decision that stays here. if req.GetPurpose() != ateapipb.ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_ATUNNEL { return nil, status.Error(codes.InvalidArgument, "unsupported actor certificate purpose") } - - if err := validateWorkerRef(req.GetWorker()); err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid worker: %v", err) - } - if req.GetExpectedActorUid() == "" { - return nil, status.Error(codes.InvalidArgument, "expected_actor_uid is required") - } actor, actorRef, err := s.authorizeActor(ctx, caller, req) if err != nil { return nil, err @@ -304,10 +304,16 @@ func authenticateAtelet(ctx context.Context) (*ateletCaller, error) { return &ateletCaller{podName: identity.PodName, nodeName: identity.NodeName}, nil } -// validateWorkerRef checks the reference to the Worker the certificate is -// minted for. Workers are global-scoped, so the reference carries no atespace. -func validateWorkerRef(worker *ateapipb.ObjectRef) error { - return resources.ValidateGlobalObjectRef(worker, field.NewPath("worker")).ToAggregate() +func validateMintJWTRequest(ctx context.Context, req *ateapipb.MintJWTRequest) field.ErrorList { + // Call the generated validation. + op := operation.Operation{Type: operation.Create} + return controlapi.Validate_MintJWTRequest(ctx, op, nil, req, nil) +} + +func validateMintCertRequest(ctx context.Context, req *ateapipb.MintCertRequest) field.ErrorList { + // Call the generated validation. + op := operation.Operation{Type: operation.Create} + return controlapi.Validate_MintCertRequest(ctx, op, nil, req, nil) } // authorizeActor resolves the actor from the authenticated worker and verifies diff --git a/cmd/ateapi/internal/actoridentity/actoridentity_test.go b/cmd/ateapi/internal/actoridentity/actoridentity_test.go index ec483e5054..672d480f1f 100644 --- a/cmd/ateapi/internal/actoridentity/actoridentity_test.go +++ b/cmd/ateapi/internal/actoridentity/actoridentity_test.go @@ -41,8 +41,14 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" + "k8s.io/apimachinery/pkg/util/validation/field" ) +func assertValidateErr(t *testing.T, got field.ErrorList, want field.ErrorList) { + t.Helper() + field.ErrorMatcher{}.ByType().ByField().ByOrigin().Test(t, want, got) +} + const ( testAtespace = "team-alpha" testActorName = "counter-1" @@ -927,3 +933,131 @@ func TestMintCertAuthorizesBeforeSigning(t *testing.T) { t.Errorf("MintCert() code = %v (err = %v), want %v", got, err, codes.PermissionDenied) } } + +func TestValidateMintJWTRequest(t *testing.T) { + // This test verifies validation of user input for minting a JWT. + validReq := func(mods ...func(req *ateapipb.MintJWTRequest)) *ateapipb.MintJWTRequest { + req := &ateapipb.MintJWTRequest{ + Audience: []string{"aud1"}, + Atespace: "as1", + ActorName: "actor1", + ActorUid: "01234567-89ab-cdef-0123-456789abcdef", + } + for _, m := range mods { + m(req) + } + return req + } + + tests := []struct { + name string + req *ateapipb.MintJWTRequest + want field.ErrorList + }{{ + "valid", + validReq(), + nil, + }, { + "missing audience", + validReq(func(r *ateapipb.MintJWTRequest) { r.Audience = nil }), + field.ErrorList{field.Required(field.NewPath("audience"), "")}, + }, { + "missing atespace", + validReq(func(r *ateapipb.MintJWTRequest) { r.Atespace = "" }), + field.ErrorList{field.Required(field.NewPath("atespace"), "")}, + }, { + "invalid atespace", + validReq(func(r *ateapipb.MintJWTRequest) { r.Atespace = "AS1" }), + field.ErrorList{field.Invalid(field.NewPath("atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + "missing actor_name", + validReq(func(r *ateapipb.MintJWTRequest) { r.ActorName = "" }), + field.ErrorList{field.Required(field.NewPath("actor_name"), "")}, + }, { + "invalid actor_name", + validReq(func(r *ateapipb.MintJWTRequest) { r.ActorName = "invalid value" }), + field.ErrorList{field.Invalid(field.NewPath("actor_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + "unspecified actor_uid", + validReq(func(r *ateapipb.MintJWTRequest) { r.ActorUid = "" }), + nil, + }, { + "invalid actor_uid", + validReq(func(r *ateapipb.MintJWTRequest) { r.ActorUid = "not a uid" }), + field.ErrorList{field.Invalid(field.NewPath("actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, validateMintJWTRequest(context.Background(), tt.req), tt.want) + }) + } +} + +func TestValidateMintCertRequest(t *testing.T) { + // This test verifies validation of user input for minting a certificate. + validReq := func(mods ...func(req *ateapipb.MintCertRequest)) *ateapipb.MintCertRequest { + req := &ateapipb.MintCertRequest{ + Worker: &ateapipb.ObjectRef{Name: "worker1"}, + CertificateSigningRequest: []byte{0x01}, + ExpectedActorUid: "01234567-89ab-cdef-0123-456789abcdef", + Purpose: ateapipb.ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_ATUNNEL, + } + for _, m := range mods { + m(req) + } + return req + } + + tests := []struct { + name string + req *ateapipb.MintCertRequest + want field.ErrorList + }{{ + "valid", + validReq(), + nil, + }, { + "missing worker", + validReq(func(r *ateapipb.MintCertRequest) { r.Worker = nil }), + field.ErrorList{field.Required(field.NewPath("worker"), "")}, + }, { + "worker.atespace must be empty", + validReq(func(r *ateapipb.MintCertRequest) { r.Worker.Atespace = "as1" }), + field.ErrorList{field.Forbidden(field.NewPath("worker", "atespace"), "")}, + }, { + "missing worker.name", + validReq(func(r *ateapipb.MintCertRequest) { r.Worker.Name = "" }), + field.ErrorList{field.Required(field.NewPath("worker", "name"), "")}, + }, { + "invalid worker.name", + validReq(func(r *ateapipb.MintCertRequest) { r.Worker.Name = "invalid value" }), + field.ErrorList{field.Invalid(field.NewPath("worker", "name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + "missing certificate_signing_request", + validReq(func(r *ateapipb.MintCertRequest) { r.CertificateSigningRequest = nil }), + field.ErrorList{field.Required(field.NewPath("certificate_signing_request"), "")}, + }, { + "missing expected_actor_uid", + validReq(func(r *ateapipb.MintCertRequest) { r.ExpectedActorUid = "" }), + field.ErrorList{field.Required(field.NewPath("expected_actor_uid"), "")}, + }, { + "invalid expected_actor_uid", + validReq(func(r *ateapipb.MintCertRequest) { r.ExpectedActorUid = "not a uid" }), + field.ErrorList{field.Invalid(field.NewPath("expected_actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + "unspecified purpose", + validReq(func(r *ateapipb.MintCertRequest) { + r.Purpose = ateapipb.ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED + }), + field.ErrorList{field.Required(field.NewPath("purpose"), "")}, + }, { + "out-of-range purpose", + validReq(func(r *ateapipb.MintCertRequest) { r.Purpose = ateapipb.ActorCertificatePurpose(99) }), + field.ErrorList{field.Invalid(field.NewPath("purpose"), nil, "").WithOrigin("maximum")}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, validateMintCertRequest(context.Background(), tt.req), tt.want) + }) + } +} diff --git a/cmd/ateapi/internal/controlapi/zz_generated.validation.go b/cmd/ateapi/internal/controlapi/zz_generated.validation.go index e09b0c8463..62e81827dc 100644 --- a/cmd/ateapi/internal/controlapi/zz_generated.validation.go +++ b/cmd/ateapi/internal/controlapi/zz_generated.validation.go @@ -736,6 +736,294 @@ func Validate_ListAtespacesRequest( return errs } +// Validate_MintCertRequest validates an instance of MintCertRequest according +// to declarative validation rules in the API schema. +func Validate_MintCertRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.MintCertRequest) (errs field.ErrorList) { + + { // field ateapipb.MintCertRequest.Worker + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.ObjectRef, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + func() { // cohort = "atespace" + earlyReturn := false + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.ForbiddenValue).MarkBeta().MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.OptionalValue).MarkBeta().MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.OptionalValue).MarkBeta().MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + }() + // call the type's validation function + errs = append(errs, Validate_ObjectRef(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintCertRequest) *ateapipb.ObjectRef { + return oldObj.Worker + }) + errs = append(errs, fn(fldPath.Child("worker"), obj.Worker, oldVal, oldObj != nil)...) + } + + { // field ateapipb.MintCertRequest.CertificateSigningRequest + fn := func( + fldPath *field.Path, + obj, oldObj []byte, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintCertRequest) []byte { + return oldObj.CertificateSigningRequest + }) + errs = append(errs, fn(fldPath.Child("certificate_signing_request"), obj.CertificateSigningRequest, oldVal, oldObj != nil)...) + } + + { // field ateapipb.MintCertRequest.ExpectedActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintCertRequest) *string { + return &oldObj.ExpectedActorUid + }) + errs = append(errs, fn(fldPath.Child("expected_actor_uid"), &obj.ExpectedActorUid, oldVal, oldObj != nil)...) + } + + { // field ateapipb.MintCertRequest.Purpose + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.ActorCertificatePurpose, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintCertRequest) *ateapipb.ActorCertificatePurpose { + return &oldObj.Purpose + }) + errs = append(errs, fn(fldPath.Child("purpose"), &obj.Purpose, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_MintJWTRequest validates an instance of MintJWTRequest according +// to declarative validation rules in the API schema. +func Validate_MintJWTRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.MintJWTRequest) (errs field.ErrorList) { + + { // field ateapipb.MintJWTRequest.Audience + fn := func( + fldPath *field.Path, + obj, oldObj []string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintJWTRequest) []string { + return oldObj.Audience + }) + errs = append(errs, fn(fldPath.Child("audience"), obj.Audience, oldVal, oldObj != nil)...) + } + + { // field ateapipb.MintJWTRequest.Atespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintJWTRequest) *string { + return &oldObj.Atespace + }) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + } + + { // field ateapipb.MintJWTRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintJWTRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateapipb.MintJWTRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintJWTRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_ObjectRef validates an instance of ObjectRef according // to declarative validation rules in the API schema. func Validate_ObjectRef( diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index a37128f43e..8f81932159 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -5092,11 +5092,22 @@ func (*DebugClearResponse) Descriptor() ([]byte, []int) { } type MintJWTRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Audience []string `protobuf:"bytes,1,rep,name=audience,proto3" json:"audience,omitempty"` - Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // The audiences the minted JWT is bound to. Tokens are only issued with + // audience bindings, so at least one is required. + // + // +k8s:required + // +k8s:listType=atomic + Audience []string `protobuf:"bytes,1,rep,name=audience,proto3" json:"audience,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // +k8s:optional + // +k8s:format=k8s-uuid + ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5234,18 +5245,28 @@ type MintCertRequest struct { // This is the one caller that recovers a Worker name from a pod certificate: // the atelet has only the worker Pod's identity to go on. Everywhere else the // name is opaque and must be carried, not reconstructed. - // +k8s:opaqueType + // + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix + // +k8s:required Worker *ObjectRef `protobuf:"bytes,1,opt,name=worker,proto3" json:"worker,omitempty"` // Request contains DER encoded bytes of a x509 certificate signing request. // The signer will ignore the contents of the CSR except to extract the // subject public key. + // + // +k8s:required CertificateSigningRequest []byte `protobuf:"bytes,2,opt,name=certificate_signing_request,json=certificateSigningRequest,proto3" json:"certificate_signing_request,omitempty"` // Actor incarnation expected by the activation. This is only a stale-request // guard: ateapi derives the actor and its identity from the worker assignment. - ExpectedActorUid string `protobuf:"bytes,3,opt,name=expected_actor_uid,json=expectedActorUid,proto3" json:"expected_actor_uid,omitempty"` - Purpose ActorCertificatePurpose `protobuf:"varint,4,opt,name=purpose,proto3,enum=ateapi.ActorCertificatePurpose" json:"purpose,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // + // +k8s:required + // +k8s:format=k8s-uuid + ExpectedActorUid string `protobuf:"bytes,3,opt,name=expected_actor_uid,json=expectedActorUid,proto3" json:"expected_actor_uid,omitempty"` + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=1 # keep this in sync with the ActorCertificatePurpose enum + Purpose ActorCertificatePurpose `protobuf:"varint,4,opt,name=purpose,proto3,enum=ateapi.ActorCertificatePurpose" json:"purpose,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *MintCertRequest) Reset() { diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 5e7b9f6797..1c1fc298cd 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -1134,10 +1134,23 @@ service ActorIdentity { } message MintJWTRequest { + // The audiences the minted JWT is bound to. Tokens are only issued with + // audience bindings, so at least one is required. + // + // +k8s:required + // +k8s:listType=atomic repeated string audience = 1; + // +k8s:required + // +k8s:format=k8s-short-name string atespace = 2; + + // +k8s:required + // +k8s:format=k8s-short-name string actor_name = 3; + + // +k8s:optional + // +k8s:format=k8s-uuid string actor_uid = 4; } @@ -1173,18 +1186,28 @@ message MintCertRequest { // This is the one caller that recovers a Worker name from a pod certificate: // the atelet has only the worker Pod's identity to go on. Everywhere else the // name is opaque and must be carried, not reconstructed. - // +k8s:opaqueType + // + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix + // +k8s:required ObjectRef worker = 1; // Request contains DER encoded bytes of a x509 certificate signing request. // The signer will ignore the contents of the CSR except to extract the // subject public key. + // + // +k8s:required bytes certificate_signing_request = 2; // Actor incarnation expected by the activation. This is only a stale-request // guard: ateapi derives the actor and its identity from the worker assignment. + // + // +k8s:required + // +k8s:format=k8s-uuid string expected_actor_uid = 3; + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=1 # keep this in sync with the ActorCertificatePurpose enum ActorCertificatePurpose purpose = 4; } From 6ceccc21d9f6e2f0c23ecc3cfe1f70adac9daee7 Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Thu, 27 Aug 2026 10:11:54 -0700 Subject: [PATCH 2/7] Add comment --- pkg/proto/ateapipb/ateapi.pb.go | 2 +- pkg/proto/ateapipb/ateapi.proto | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index 8f81932159..2627d4bc87 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -427,7 +427,7 @@ type ActorCertificatePurpose int32 const ( ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED ActorCertificatePurpose = 0 - ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_ATUNNEL ActorCertificatePurpose = 1 + ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_ATUNNEL ActorCertificatePurpose = 1 // Keep this in sync with MintCertRequest.purpose's maximum. ) // Enum value maps for ActorCertificatePurpose. diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 1c1fc298cd..ae9c1f3f91 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -1214,6 +1214,7 @@ message MintCertRequest { enum ActorCertificatePurpose { ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED = 0; ACTOR_CERTIFICATE_PURPOSE_ATUNNEL = 1; + // Keep this in sync with MintCertRequest.purpose's maximum. } message MintCertResponse { From 1ce8eec6934a800534d9c0d86ab72feedc4e1f49 Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Thu, 27 Aug 2026 17:12:07 -0700 Subject: [PATCH 3/7] fix test case --- cmd/ateapi/internal/actoridentity/actoridentity_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/ateapi/internal/actoridentity/actoridentity_test.go b/cmd/ateapi/internal/actoridentity/actoridentity_test.go index 672d480f1f..1267c2e5dd 100644 --- a/cmd/ateapi/internal/actoridentity/actoridentity_test.go +++ b/cmd/ateapi/internal/actoridentity/actoridentity_test.go @@ -796,7 +796,9 @@ func TestMintCertActorUID(t *testing.T) { wantCode codes.Code }{ "Matching": {requestUID: func(actorUID string) string { return actorUID }, wantCode: codes.OK}, - "Stale": {requestUID: func(string) string { return "uid-of-a-previous-incarnation" }, wantCode: codes.FailedPrecondition}, + // The stale uid is well-formed on purpose: a malformed one is rejected + // as INVALID_ARGUMENT by request validation before the guard runs. + "Stale": {requestUID: func(string) string { return "9d1f7b06-3c58-4a2e-8b40-5f7c1e9a2d63" }, wantCode: codes.FailedPrecondition}, } { t.Run(name, func(t *testing.T) { leaf, actorUID, err := mintCertFor(t, func(actorUID string) *ateapipb.MintCertRequest { From 4b4ca066a9cfb84b6b5f998ff263bd95a24e96ed Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Thu, 27 Aug 2026 17:37:20 -0700 Subject: [PATCH 4/7] Fix apitool exemptions --- tools/apitool/exemptions.json | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/tools/apitool/exemptions.json b/tools/apitool/exemptions.json index f7938123e6..61de307966 100644 --- a/tools/apitool/exemptions.json +++ b/tools/apitool/exemptions.json @@ -114,16 +114,6 @@ "subject": "ateapi.ArchAssets", "message": "message has no doc comment" }, - { - "rule": "documented", - "subject": "ateapi.Capabilities.add", - "message": "field has no doc comment" - }, - { - "rule": "documented", - "subject": "ateapi.Capabilities.drop", - "message": "field has no doc comment" - }, { "rule": "documented", "subject": "ateapi.Container.env", @@ -274,11 +264,6 @@ "subject": "ateapi.HTTPGetAction.port", "message": "field has no doc comment" }, - { - "rule": "documented", - "subject": "ateapi.ImageVolumeSource.reference", - "message": "field has no doc comment" - }, { "rule": "documented", "subject": "ateapi.KubeNamespacedObjectRef", @@ -429,11 +414,6 @@ "subject": "ateapi.SandboxAssets.sandbox_class", "message": "field has no doc comment" }, - { - "rule": "documented", - "subject": "ateapi.SecurityContext.capabilities", - "message": "field has no doc comment" - }, { "rule": "documented", "subject": "ateapi.SnapshotContentScope", @@ -479,11 +459,6 @@ "subject": "ateapi.Volume.external_volume_template", "message": "field has no doc comment" }, - { - "rule": "documented", - "subject": "ateapi.Volume.image", - "message": "field has no doc comment" - }, { "rule": "documented", "subject": "ateapi.Worker.ip", From 45acedff8af4a9bc8d34e9e1eba2c9a69b482ffa Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Fri, 28 Aug 2026 10:46:25 -0700 Subject: [PATCH 5/7] Update apitool exemptions --- tools/apitool/exemptions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/apitool/exemptions.json b/tools/apitool/exemptions.json index 9754b92c44..0341a503df 100644 --- a/tools/apitool/exemptions.json +++ b/tools/apitool/exemptions.json @@ -639,4 +639,4 @@ "subject": "ateapi.WorkerStatus.state", "message": "field has no doc comment" } -] \ No newline at end of file +] From b613c7a1a3008449ba499c3b65dc397e663aa8b2 Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Fri, 28 Aug 2026 15:54:31 -0700 Subject: [PATCH 6/7] nit --- cmd/ateapi/internal/actoridentity/actoridentity_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/ateapi/internal/actoridentity/actoridentity_test.go b/cmd/ateapi/internal/actoridentity/actoridentity_test.go index 7cfcdca86e..ef1ebe49f8 100644 --- a/cmd/ateapi/internal/actoridentity/actoridentity_test.go +++ b/cmd/ateapi/internal/actoridentity/actoridentity_test.go @@ -783,8 +783,6 @@ func TestMintCertActorUID(t *testing.T) { wantCode codes.Code }{ "Matching": {requestUID: func(actorUID string) string { return actorUID }, wantCode: codes.OK}, - // The stale uid is well-formed on purpose: a malformed one is rejected - // as INVALID_ARGUMENT by request validation before the guard runs. "Stale": {requestUID: func(string) string { return "9d1f7b06-3c58-4a2e-8b40-5f7c1e9a2d63" }, wantCode: codes.FailedPrecondition}, } { t.Run(name, func(t *testing.T) { From d7d731ec6874a0dbb4d66212d213069d1859cdb2 Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Mon, 31 Aug 2026 14:01:36 -0700 Subject: [PATCH 7/7] Add bounds for audience and certificate --- .../actoridentity/actoridentity_test.go | 27 ++++++++++++++++++- cmd/ateapi/internal/controlapi/validate.go | 11 ++++++++ .../controlapi/zz_generated.validation.go | 18 +++++++++++++ pkg/proto/ateapipb/ateapi.pb.go | 5 +++- pkg/proto/ateapipb/ateapi.proto | 5 +++- 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/cmd/ateapi/internal/actoridentity/actoridentity_test.go b/cmd/ateapi/internal/actoridentity/actoridentity_test.go index ef1ebe49f8..52d0964572 100644 --- a/cmd/ateapi/internal/actoridentity/actoridentity_test.go +++ b/cmd/ateapi/internal/actoridentity/actoridentity_test.go @@ -21,9 +21,11 @@ import ( "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "fmt" "math/big" "net/url" "path" + "strings" "testing" "time" @@ -783,7 +785,7 @@ func TestMintCertActorUID(t *testing.T) { wantCode codes.Code }{ "Matching": {requestUID: func(actorUID string) string { return actorUID }, wantCode: codes.OK}, - "Stale": {requestUID: func(string) string { return "9d1f7b06-3c58-4a2e-8b40-5f7c1e9a2d63" }, wantCode: codes.FailedPrecondition}, + "Stale": {requestUID: func(string) string { return "9d1f7b06-3c58-4a2e-8b40-5f7c1e9a2d63" }, wantCode: codes.FailedPrecondition}, } { t.Run(name, func(t *testing.T) { leaf, actorUID, err := mintCertFor(t, func(actorUID string) *ateapipb.MintCertRequest { @@ -958,6 +960,25 @@ func TestValidateMintJWTRequest(t *testing.T) { "missing audience", validReq(func(r *ateapipb.MintJWTRequest) { r.Audience = nil }), field.ErrorList{field.Required(field.NewPath("audience"), "")}, + }, { + "too many audiences", + validReq(func(r *ateapipb.MintJWTRequest) { + r.Audience = make([]string, 17) + for i := range r.Audience { + r.Audience[i] = fmt.Sprintf("https://svc-%d.example.com", i) + } + }), + field.ErrorList{field.TooMany(field.NewPath("audience"), 17, 16).WithOrigin("maxItems")}, + }, { + "duplicate audience entry", + validReq(func(r *ateapipb.MintJWTRequest) { + r.Audience = []string{"https://a.example.com", "https://a.example.com"} + }), + field.ErrorList{field.Duplicate(field.NewPath("audience").Index(1), nil)}, + }, { + "audience entry too long", + validReq(func(r *ateapipb.MintJWTRequest) { r.Audience = []string{strings.Repeat("a", 513)} }), + field.ErrorList{field.TooLong(field.NewPath("audience").Index(0), nil, 512).WithOrigin("maxLength")}, }, { "missing atespace", validReq(func(r *ateapipb.MintJWTRequest) { r.Atespace = "" }), @@ -1013,6 +1034,10 @@ func TestValidateMintCertRequest(t *testing.T) { "valid", validReq(), nil, + }, { + "oversized certificate_signing_request", + validReq(func(r *ateapipb.MintCertRequest) { r.CertificateSigningRequest = make([]byte, 16385) }), + field.ErrorList{field.TooLong(field.NewPath("certificate_signing_request"), nil, 16384)}, }, { "missing worker", validReq(func(r *ateapipb.MintCertRequest) { r.Worker = nil }), diff --git a/cmd/ateapi/internal/controlapi/validate.go b/cmd/ateapi/internal/controlapi/validate.go index 68358d20ee..630da536f7 100644 --- a/cmd/ateapi/internal/controlapi/validate.go +++ b/cmd/ateapi/internal/controlapi/validate.go @@ -97,3 +97,14 @@ func ValidateCustom_UpdateActorRequest_Actor(ctx context.Context, op operation.O func ValidateCustom_WorkerAssignment_WorkerPodIp(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { return validation.IsValidIP(fldPath, *value) } + +// maxCSRBytes bounds MintCertRequest's CSR. Real CSRs are a few KB; this is +// a guardrail, applied here because maxLength does not support bytes fields. +const maxCSRBytes = 16384 + +func ValidateCustom_MintCertRequest_CertificateSigningRequest(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []byte) field.ErrorList { + if len(value) > maxCSRBytes { + return field.ErrorList{field.TooLong(fldPath, nil, maxCSRBytes)} + } + return nil +} diff --git a/cmd/ateapi/internal/controlapi/zz_generated.validation.go b/cmd/ateapi/internal/controlapi/zz_generated.validation.go index 35a2eea17c..3e04ef47a4 100644 --- a/cmd/ateapi/internal/controlapi/zz_generated.validation.go +++ b/cmd/ateapi/internal/controlapi/zz_generated.validation.go @@ -1760,6 +1760,10 @@ func Validate_MintCertRequest( if earlyReturn { return // do not proceed } + // custom validation + if e := ValidateCustom_MintCertRequest_CertificateSigningRequest(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } return } oldVal := safe.Field(oldObj, @@ -1858,6 +1862,10 @@ func Validate_MintJWTRequest( } // call field-attached validations earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 16).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true @@ -1865,6 +1873,16 @@ func Validate_MintJWTRequest( if earlyReturn { return // do not proceed } + if e := validate.EachValSliceVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, nil, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 512) + }); len(e) != 0 { + errs = append(errs, e...) + } + // lists with set semantics require unique values + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { + errs = append(errs, e...) + } return } oldVal := safe.Field(oldObj, diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index 71815cadb7..f84c62275e 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -6080,7 +6080,9 @@ type MintJWTRequest struct { // audience bindings, so at least one is required. // // +k8s:required - // +k8s:listType=atomic + // +k8s:maxItems=16 # guardrail; tokens realistically bind a handful of audiences + // +k8s:listType=set + // +k8s:eachVal=+k8s:maxLength=512 # audiences are caller-defined URIs; bound only Audience []string `protobuf:"bytes,1,rep,name=audience,proto3" json:"audience,omitempty"` // +k8s:required // +k8s:format=k8s-short-name @@ -6235,6 +6237,7 @@ type MintCertRequest struct { // subject public key. // // +k8s:required + // +k8s:customValidation # size bound; maxLength is string-only CertificateSigningRequest []byte `protobuf:"bytes,2,opt,name=certificate_signing_request,json=certificateSigningRequest,proto3" json:"certificate_signing_request,omitempty"` // Actor incarnation expected by the activation. This is only a stale-request // guard: ateapi derives the actor and its identity from the worker assignment. diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 2aebf5ed40..6ca24e44fc 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -1420,7 +1420,9 @@ message MintJWTRequest { // audience bindings, so at least one is required. // // +k8s:required - // +k8s:listType=atomic + // +k8s:maxItems=16 # guardrail; tokens realistically bind a handful of audiences + // +k8s:listType=set + // +k8s:eachVal=+k8s:maxLength=512 # audiences are caller-defined URIs; bound only repeated string audience = 1; // +k8s:required @@ -1478,6 +1480,7 @@ message MintCertRequest { // subject public key. // // +k8s:required + // +k8s:customValidation # size bound; maxLength is string-only bytes certificate_signing_request = 2; // Actor incarnation expected by the activation. This is only a stale-request