From 4cf99348145b355e333d472bdc9de75438f2b1bf Mon Sep 17 00:00:00 2001 From: Youssef Yamout Date: Thu, 30 Apr 2026 12:04:56 -0400 Subject: [PATCH 1/4] Add IP SAN support to bootz test certificates --- testdata/artifacts.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/testdata/artifacts.go b/testdata/artifacts.go index 45489286..52d6e0d9 100644 --- a/testdata/artifacts.go +++ b/testdata/artifacts.go @@ -28,6 +28,7 @@ import ( "encoding/xml" "fmt" "math/big" + "net" "time" "github.com/openconfig/bootz/server/service" @@ -58,11 +59,20 @@ type Inner struct { DomainCertRevocationChecks bool `json:"domain-cert-revocation-checks" xml:"domain-cert-revocation-checks"` } +func certificateSANs(serverName string) ([]string, []net.IP) { + if ip := net.ParseIP(serverName); ip != nil { + return nil, []net.IP{ip} + } + return []string{serverName}, nil +} + // NewCertificateAuthority creates a new self-signed CA for the chosen organization. func NewCertificateAuthority(commonName, org, serverName string) (*x509.Certificate, *rsa.PrivateKey, error) { + dnsNames, ipAddresses := certificateSANs(serverName) // Create the certificate authority. ca := &x509.Certificate{ - DNSNames: []string{serverName}, + DNSNames: dnsNames, + IPAddresses: ipAddresses, SerialNumber: big.NewInt(int64(time.Now().Year())), Subject: pkix.Name{ CommonName: commonName, @@ -100,9 +110,11 @@ func NewCertificateAuthority(commonName, org, serverName string) (*x509.Certific // NewSignedCertificate creates a new cert/private keypair signed by the provided Certificate Authority. func NewSignedCertificate(commonName, org, serverName string, ca *x509.Certificate, caPrivateKey crypto.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) { + dnsNames, ipAddresses := certificateSANs(serverName) // Create the certificate template. Geographic information is the same as the Certificate Authority by default. template := &x509.Certificate{ - DNSNames: []string{serverName}, + DNSNames: dnsNames, + IPAddresses: ipAddresses, SerialNumber: big.NewInt(int64(time.Now().Year())), Subject: pkix.Name{ CommonName: commonName, From a7aab58472011c448d388a589d352b74f0078e81 Mon Sep 17 00:00:00 2001 From: Youssef Yamout Date: Thu, 30 Apr 2026 12:04:56 -0400 Subject: [PATCH 2/4] Allow disabling deprecated bootstrap stream --- server/server.go | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/server/server.go b/server/server.go index 554542d4..0a6fff1e 100644 --- a/server/server.go +++ b/server/server.go @@ -32,7 +32,9 @@ import ( "github.com/openconfig/bootz/server/entitymanager" "github.com/openconfig/bootz/server/service" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" bpb "github.com/openconfig/bootz/proto/bootz" ) @@ -88,9 +90,37 @@ type InterceptorOpts struct { func (*InterceptorOpts) isbootzServerOpts() {} +// DisableBootstrapStream disables the deprecated BootstrapStream RPC while +// keeping unary RPCs available. +type DisableBootstrapStream struct{} + +func (*DisableBootstrapStream) isbootzServerOpts() {} + +type bootstrapServer struct { + bpb.UnimplementedBootstrapServer + service *service.Service + disableBootstrapStream bool +} + +func (s *bootstrapServer) GetBootstrapData(ctx context.Context, req *bpb.GetBootstrapDataRequest) (*bpb.GetBootstrapDataResponse, error) { + return s.service.GetBootstrapData(ctx, req) +} + +func (s *bootstrapServer) ReportStatus(ctx context.Context, req *bpb.ReportStatusRequest) (*bpb.EmptyResponse, error) { + return s.service.ReportStatus(ctx, req) +} + +func (s *bootstrapServer) BootstrapStream(stream bpb.Bootstrap_BootstrapStreamServer) error { + if s.disableBootstrapStream { + return status.Error(codes.Unimplemented, "BootstrapStream disabled") + } + return s.service.BootstrapStream(stream) +} + // NewServer start a new Bootz gRPC , dhcp, and image server based on specified flags. func NewServer(bootzAddr string, em *entitymanager.InMemoryEntityManager, sa *service.SecurityArtifacts, opts ...bootzServerOpts) (*Server, error) { var interceptor grpc.ServerOption + disableBootstrapStream := false server := &Server{} for _, opt := range opts { switch opt := opt.(type) { @@ -102,6 +132,8 @@ func NewServer(bootzAddr string, em *entitymanager.InMemoryEntityManager, sa *se server.httpSrv = StartImageServer(opt) case *InterceptorOpts: interceptor = grpc.UnaryInterceptor(opt.BootzInterceptor) + case *DisableBootstrapStream: + disableBootstrapStream = true default: continue } @@ -124,7 +156,10 @@ func NewServer(bootzAddr string, em *entitymanager.InMemoryEntityManager, sa *se s = grpc.NewServer(grpc.Creds(credentials.NewTLS(tls))) } - bpb.RegisterBootstrapServer(s, c) + bpb.RegisterBootstrapServer(s, &bootstrapServer{ + service: c, + disableBootstrapStream: disableBootstrapStream, + }) lis, err := net.Listen("tcp", bootzAddr) if err != nil { From 62b154d50789a7ea882df73fcde160701ad47987 Mon Sep 17 00:00:00 2001 From: Youssef Yamout Date: Thu, 30 Apr 2026 12:04:56 -0400 Subject: [PATCH 3/4] Plumb certz profiles through bootstrap data --- server/entitymanager/entitymanager.go | 4 +++- server/entitymanager/proto/entity.proto | 4 +++- server/entitymanager/proto/entity/entity.pb.go | 13 +++++++++++-- server/service/service.go | 1 + 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/server/entitymanager/entitymanager.go b/server/entitymanager/entitymanager.go index 0c95d8d2..97f7551c 100644 --- a/server/entitymanager/entitymanager.go +++ b/server/entitymanager/entitymanager.go @@ -89,6 +89,7 @@ func (m *InMemoryEntityManager) ResolveChassis(ctx context.Context, lookup *serv ControlCards: cards, BootConfig: bootCfg, Authz: authzConf, + CertzProfiles: chassis.GetConfig().GetGnsiConfig().GetCertzProfiles(), BootloaderPasswordHash: chassis.GetBootloaderPasswordHash(), }, nil } @@ -195,7 +196,8 @@ func (m *InMemoryEntityManager) GetBootstrapData(ctx context.Context, chassis *s BootConfig: chassis.BootConfig, Credentials: &bpb.Credentials{}, // TODO: Populate pathz, authz and certificates. - Authz: chassis.Authz, + Authz: chassis.Authz, + CertzProfiles: chassis.CertzProfiles, }, nil } diff --git a/server/entitymanager/proto/entity.proto b/server/entitymanager/proto/entity.proto index 767e8304..cc6be3ad 100644 --- a/server/entitymanager/proto/entity.proto +++ b/server/entitymanager/proto/entity.proto @@ -103,6 +103,9 @@ message GNSIConfig { // gnsi credential config bootz.Credentials credentials = 8; + // Certz profiles to create, including the target SSL profile ID. + bootz.CertzProfiles certz_profiles = 9; + } message DHCPConfig { @@ -163,4 +166,3 @@ message Chassis { } - diff --git a/server/entitymanager/proto/entity/entity.pb.go b/server/entitymanager/proto/entity/entity.pb.go index 2427613d..e7445569 100755 --- a/server/entitymanager/proto/entity/entity.pb.go +++ b/server/entitymanager/proto/entity/entity.pb.go @@ -7,6 +7,9 @@ package entity import ( + reflect "reflect" + sync "sync" + bootz "github.com/openconfig/bootz/proto/bootz" authz "github.com/openconfig/gnsi/authz" certz "github.com/openconfig/gnsi/certz" @@ -14,8 +17,6 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" - reflect "reflect" - sync "sync" ) const ( @@ -283,6 +284,7 @@ type GNSIConfig struct { CertzUploadFile string `protobuf:"bytes,6,opt,name=certz_upload_file,json=certzUploadFile,proto3" json:"certz_upload_file,omitempty"` CredentialsFile string `protobuf:"bytes,7,opt,name=credentials_file,json=credentialsFile,proto3" json:"credentials_file,omitempty"` Credentials *bootz.Credentials `protobuf:"bytes,8,opt,name=credentials,proto3" json:"credentials,omitempty"` + CertzProfiles *bootz.CertzProfiles `protobuf:"bytes,9,opt,name=certz_profiles,json=certzProfiles,proto3" json:"certz_profiles,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -373,6 +375,13 @@ func (x *GNSIConfig) GetCredentials() *bootz.Credentials { return nil } +func (x *GNSIConfig) GetCertzProfiles() *bootz.CertzProfiles { + if x != nil { + return x.CertzProfiles + } + return nil +} + type DHCPConfig struct { state protoimpl.MessageState `protogen:"open.v1"` HardwareAddress string `protobuf:"bytes,1,opt,name=hardware_address,json=hardwareAddress,proto3" json:"hardware_address,omitempty"` diff --git a/server/service/service.go b/server/service/service.go index 89a6ad3e..020eeb37 100644 --- a/server/service/service.go +++ b/server/service/service.go @@ -98,6 +98,7 @@ type Chassis struct { // cases where this data should be hardcoded e.g. for testing. BootConfig *bpb.BootConfig Authz *apb.UploadRequest + CertzProfiles *bpb.CertzProfiles BootloaderPasswordHash string } From 1ca8730c2ddec4e88f314a1f87c06b1d88fd16b7 Mon Sep 17 00:00:00 2001 From: Youssef Yamout Date: Thu, 7 May 2026 16:45:59 -0400 Subject: [PATCH 4/4] Propagate credz and pathz in bootstrap data --- server/entitymanager/entitymanager.go | 60 ++++++++++++++++++++-- server/entitymanager/entitymanager_test.go | 42 ++++++++++++++- server/service/service.go | 3 ++ 3 files changed, 100 insertions(+), 5 deletions(-) diff --git a/server/entitymanager/entitymanager.go b/server/entitymanager/entitymanager.go index 97f7551c..c5d5d813 100644 --- a/server/entitymanager/entitymanager.go +++ b/server/entitymanager/entitymanager.go @@ -36,6 +36,7 @@ import ( bpb "github.com/openconfig/bootz/proto/bootz" epb "github.com/openconfig/bootz/server/entitymanager/proto/entity" apb "github.com/openconfig/gnsi/authz" + ppb "github.com/openconfig/gnsi/pathz" ) const defaultRealm = "prod" @@ -74,6 +75,14 @@ func (m *InMemoryEntityManager) ResolveChassis(ctx context.Context, lookup *serv if err != nil { return nil, err } + credzConf, err := m.populateCredentialsConfig(chassis) + if err != nil { + return nil, err + } + pathzConf, err := m.populatePathzConfig(chassis) + if err != nil { + return nil, err + } authzConf, err := m.populateAuthzConfig(chassis) if err != nil { return nil, err @@ -88,6 +97,8 @@ func (m *InMemoryEntityManager) ResolveChassis(ctx context.Context, lookup *serv Serial: chassis.GetSerialNumber(), ControlCards: cards, BootConfig: bootCfg, + Credentials: credzConf, + Pathz: pathzConf, Authz: authzConf, CertzProfiles: chassis.GetConfig().GetGnsiConfig().GetCertzProfiles(), BootloaderPasswordHash: chassis.GetBootloaderPasswordHash(), @@ -161,6 +172,48 @@ func (m *InMemoryEntityManager) populateAuthzConfig(ch *epb.Chassis) (*apb.Uploa return gnsiAuthzReq, nil } +func (m *InMemoryEntityManager) populatePathzConfig(ch *epb.Chassis) (*ppb.UploadRequest, error) { + gnsiConf := ch.GetConfig().GetGnsiConfig() + gnsiPathzReq := gnsiConf.GetPathzUpload() + gnsiPathzReqFile := gnsiConf.GetPathzUploadFile() + if gnsiPathzReq.GetVersion() != "" && gnsiPathzReq.GetPolicy() != nil { + return gnsiPathzReq, nil + } + if gnsiPathzReqFile == "" { + return nil, nil + } + data, err := os.ReadFile(gnsiPathzReqFile) + if err != nil { + return nil, status.Errorf(codes.Internal, "Error opening file %s: %v", gnsiPathzReqFile, err) + } + gnsiPathzReq = &ppb.UploadRequest{} + if err := prototext.Unmarshal(data, gnsiPathzReq); err != nil { + return nil, status.Errorf(codes.Internal, "File %s config is not a valid pathz Upload Request: %v", gnsiPathzReqFile, err) + } + return gnsiPathzReq, nil +} + +func (m *InMemoryEntityManager) populateCredentialsConfig(ch *epb.Chassis) (*bpb.Credentials, error) { + gnsiConf := ch.GetConfig().GetGnsiConfig() + gnsiCredzReq := gnsiConf.GetCredentials() + gnsiCredzReqFile := gnsiConf.GetCredentialsFile() + if len(gnsiCredzReq.GetCredentials()) > 0 || len(gnsiCredzReq.GetUsers()) > 0 || len(gnsiCredzReq.GetPasswords()) > 0 { + return gnsiCredzReq, nil + } + if gnsiCredzReqFile == "" { + return nil, nil + } + data, err := os.ReadFile(gnsiCredzReqFile) + if err != nil { + return nil, status.Errorf(codes.Internal, "Error opening file %s: %v", gnsiCredzReqFile, err) + } + gnsiCredzReq = &bpb.Credentials{} + if err := prototext.Unmarshal(data, gnsiCredzReq); err != nil { + return nil, status.Errorf(codes.Internal, "File %s config is not valid Bootz credentials: %v", gnsiCredzReqFile, err) + } + return gnsiCredzReq, nil +} + func populateBootConfig(conf *epb.BootConfig) (*bpb.BootConfig, error) { bootConfig := &bpb.BootConfig{} if conf.GetOcConfigFile() != "" { @@ -187,16 +240,15 @@ func populateBootConfig(conf *epb.BootConfig) (*bpb.BootConfig, error) { // GetBootstrapData fetches and returns the bootstrap data response from the server. func (m *InMemoryEntityManager) GetBootstrapData(ctx context.Context, chassis *service.Chassis, serial string) (*bpb.BootstrapDataResponse, error) { - // TODO: Populate gnsi config return &bpb.BootstrapDataResponse{ SerialNum: serial, IntendedImage: chassis.SoftwareImage, BootPasswordHash: chassis.BootloaderPasswordHash, ServerTrustCert: base64.StdEncoding.EncodeToString(m.secArtifacts.TrustAnchor.Raw), BootConfig: chassis.BootConfig, - Credentials: &bpb.Credentials{}, - // TODO: Populate pathz, authz and certificates. - Authz: chassis.Authz, + Credentials: chassis.Credentials, + Pathz: chassis.Pathz, + Authz: chassis.Authz, CertzProfiles: chassis.CertzProfiles, }, nil } diff --git a/server/entitymanager/entitymanager_test.go b/server/entitymanager/entitymanager_test.go index fd4bb07e..5c5d0a90 100644 --- a/server/entitymanager/entitymanager_test.go +++ b/server/entitymanager/entitymanager_test.go @@ -34,6 +34,9 @@ import ( bpb "github.com/openconfig/bootz/proto/bootz" epb "github.com/openconfig/bootz/server/entitymanager/proto/entity" apb "github.com/openconfig/gnsi/authz" + cpb "github.com/openconfig/gnsi/certz" + kpb "github.com/openconfig/gnsi/credentialz" + ppb "github.com/openconfig/gnsi/pathz" ) // MustMarshalBootstrapDataSigned is a helper function that marshals a BootstrapDataSigned message. @@ -480,11 +483,30 @@ func TestGetBootstrapData(t *testing.T) { VendorConfig: []byte(""), OcConfig: []byte(""), }, + Credentials: &bpb.Credentials{ + Credentials: []*kpb.AuthorizedKeysRequest{{ + Credentials: []*kpb.AccountCredentials{{ + Account: "gnetch", + AuthorizedKeys: []*kpb.AccountCredentials_AuthorizedKey{{ + AuthorizedKey: []byte("AAAA-test-key"), + }}, + }}, + }}, + }, + Pathz: &ppb.UploadRequest{ + Version: "pathz-v1", + }, Authz: &apb.UploadRequest{ Version: "v0.1694813669807611349", CreatedOn: 1694813669807, Policy: "{\"name\":\"default\",\"request\":{\"paths\":[\"*\"]},\"source\":{\"principals\":[\"cafyauto\"]}}", }, + CertzProfiles: &bpb.CertzProfiles{ + Profiles: []*bpb.CertzProfile{{ + SslProfileId: "tls", + Certz: &cpb.UploadRequest{}, + }}, + }, }, want: &bpb.BootstrapDataResponse{ SerialNum: "123", @@ -501,12 +523,30 @@ func TestGetBootstrapData(t *testing.T) { VendorConfig: []byte(""), OcConfig: []byte(""), }, - Credentials: &bpb.Credentials{}, + Credentials: &bpb.Credentials{ + Credentials: []*kpb.AuthorizedKeysRequest{{ + Credentials: []*kpb.AccountCredentials{{ + Account: "gnetch", + AuthorizedKeys: []*kpb.AccountCredentials_AuthorizedKey{{ + AuthorizedKey: []byte("AAAA-test-key"), + }}, + }}, + }}, + }, + Pathz: &ppb.UploadRequest{ + Version: "pathz-v1", + }, Authz: &apb.UploadRequest{ Version: "v0.1694813669807611349", CreatedOn: 1694813669807, Policy: "{\"name\":\"default\",\"request\":{\"paths\":[\"*\"]},\"source\":{\"principals\":[\"cafyauto\"]}}", }, + CertzProfiles: &bpb.CertzProfiles{ + Profiles: []*bpb.CertzProfile{{ + SslProfileId: "tls", + Certz: &cpb.UploadRequest{}, + }}, + }, }, wantErr: false, }, diff --git a/server/service/service.go b/server/service/service.go index 020eeb37..4a002544 100644 --- a/server/service/service.go +++ b/server/service/service.go @@ -32,6 +32,7 @@ import ( log "github.com/golang/glog" bpb "github.com/openconfig/bootz/proto/bootz" apb "github.com/openconfig/gnsi/authz" + ppb "github.com/openconfig/gnsi/pathz" ) // OVList is a mapping of control card serial number to ownership voucher. @@ -97,6 +98,8 @@ type Chassis struct { // The below fields are normally unset and are primarily used for // cases where this data should be hardcoded e.g. for testing. BootConfig *bpb.BootConfig + Credentials *bpb.Credentials + Pathz *ppb.UploadRequest Authz *apb.UploadRequest CertzProfiles *bpb.CertzProfiles BootloaderPasswordHash string