diff --git a/client/README.md b/client/README.md index 0cb0bd3..c0b4f84 100644 --- a/client/README.md +++ b/client/README.md @@ -1,11 +1,21 @@ -# Bootz Client Emulator +# Bootz Client Reference emulation -The code located in this directory is intended to emulator a typical Bootz +The code located in this directory is intended to emulate a typical Bootz client. Where appropriate, some device-specific functions such as downloading and image or applying a config are mocked out and are simply logged. ## Usage +First, make sure the server is running. See [server readme](../server/README.md). + +To run the client, build it and then run it with at least the `port` flag specified. The default value of `root_ca_cert_path` works for this implementation. We recommend using the flag `alsologtostderr` to get a verbose output. + +```shell +cd client +go build client.go +./client -port 8080 -alsologtostderr +``` + ### Flags * `port`: The port to listen to the Bootz Server on localhost. @@ -14,9 +24,3 @@ and image or applying a config are mocked out and are simply logged. * `root_ca_cert_path`: A path to a file that contains a PEM encoded certificate for the trusted ZTP Signing authority. This certificate will be used to validate the ownership voucher. - -### Root CA - -Included in this directory is a file named `ca.pem`. This file should contain -the PEM encoded certificate that the device will use to validate ownership -vouchers. diff --git a/client/client.go b/client/client.go index 252c224..257b598 100644 --- a/client/client.go +++ b/client/client.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha256" + "crypto/tls" "crypto/x509" "encoding/base64" "encoding/json" @@ -21,7 +22,7 @@ import ( "github.com/openconfig/bootz/proto/bootz" "go.mozilla.org/pkcs7" "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/credentials" "google.golang.org/protobuf/proto" ) @@ -29,9 +30,10 @@ import ( const nonceLength = 16 var ( - insecureBoot = flag.Bool("insecure_boot", false, "Whether to start the emulated device in non-secure mode. This informs Bootz server to not provide ownership certificates or vouchers.") - port = flag.String("port", "", "The port to listen to on localhost for the bootz server.") - rootCA = flag.String("root_ca_cert_path", "../testdata/ca.pem", "The relative path to a file contained a PEM encoded certificate for the manufacturer CA.") + verifyTLSCert = flag.Bool("verify_tls_cert", false, "Whether to verify the TLS certificate presented by the Bootz server. If false, all TLS connections are implicity trusted.") + insecureBoot = flag.Bool("insecure_boot", false, "Whether to start the emulated device in non-secure mode. This informs Bootz server to not provide ownership certificates or vouchers.") + port = flag.String("port", "", "The port to listen to on localhost for the bootz server.") + rootCA = flag.String("root_ca_cert_path", "../testdata/vendorca_pub.pem", "The relative path to a file containing a PEM encoded certificate for the manufacturer CA.") ) type OwnershipVoucher struct { @@ -134,11 +136,15 @@ func validateArtifacts(serialNumber string, resp *bootz.GetBootstrapDataResponse return err } hashed := sha256.Sum256(signedResponseBytes) + decodedSig, err := base64.StdEncoding.DecodeString(resp.GetResponseSignature()) + if err != nil { + return err + } // Verify the signature with the ownership certificate's public key. Currently only RSA keys are supported. switch pub := ocCert.PublicKey.(type) { case *rsa.PublicKey: - err = rsa.VerifyPKCS1v15(pub, crypto.SHA256, hashed[:], []byte(resp.GetResponseSignature())) + err = rsa.VerifyPKCS1v15(pub, crypto.SHA256, hashed[:], decodedSig) if err != nil { return fmt.Errorf("signature not verified: %v", err) } @@ -165,7 +171,7 @@ func generateNonce() (string, error) { if err != nil { return "", err } - return string(b), nil + return base64.StdEncoding.EncodeToString(b), nil } func main() { @@ -219,8 +225,8 @@ func main() { // 2. Bootstrapping Service // Device initiates a TLS-secured gRPC connection with the Bootz server. - // TODO: Make this use TLS. - conn, err := grpc.Dial(bootzAddress, grpc.WithTransportCredentials(insecure.NewCredentials())) + tlsConfig := &tls.Config{InsecureSkipVerify: !*verifyTLSCert} + conn, err := grpc.Dial(bootzAddress, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) if err != nil { log.Exitf("Unable to connect to Bootstrap Server: %v", err) } diff --git a/go.mod b/go.mod index a565cef..e856ed0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/golang/glog v1.1.2 + github.com/google/go-cmp v0.5.9 github.com/openconfig/gnmi v0.0.0-20220617175856-41246b1b3507 github.com/openconfig/gnsi v1.2.1 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..bca52df --- /dev/null +++ b/server/README.md @@ -0,0 +1,21 @@ +# Bootz Server Reference Implementation + +The code located in this directory is intended to emulate a typical Bootz +server. + +## Usage + +To run the server, build it and then run it with at least the `port` flag specified. The default value of `artifact_dir` works for this implementation. We recommend using the flag `alsologtostderr` to get a verbose output. + +```shell +cd server +go build server.go +./server -port 8080 -alsologtostderr +``` + +Once running, run the client implementation in another terminal. See [client readme](../client/README.md). + +### Flags + +* `port`: The port to start to the Bootz Server on localhost. +* `artifact_dir`: A relative directory to look for security artifacts. See README.md in the testdata directory for an explanation of these. \ No newline at end of file diff --git a/server/entitymanager/entitymanager.go b/server/entitymanager/entitymanager.go index 1af777e..7e2a1da 100644 --- a/server/entitymanager/entitymanager.go +++ b/server/entitymanager/entitymanager.go @@ -2,38 +2,163 @@ package entitymanager import ( + "crypto" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "sync" + "github.com/openconfig/bootz/proto/bootz" "github.com/openconfig/bootz/server/service" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + + log "github.com/golang/glog" ) // InMemoryEntityManager provides a simple in memory handler // for Entities. -type InMemoryEntityManager struct{} +type InMemoryEntityManager struct { + mu sync.Mutex + // inventory represents an organization's inventory of owned chassis. + chassisInventory map[service.EntityLookup]*service.ChassisEntity + // represents the current status of known control cards + controlCardStatuses map[string]bootz.ControlCardState_ControlCardStatus + // stores the security artifacts required by Bootz Server (OVs, OC and PDC) + artifacts *service.SecurityArtifacts +} // ResolveChassis returns an entity based on the provided lookup. func (m *InMemoryEntityManager) ResolveChassis(lookup *service.EntityLookup) (*service.ChassisEntity, error) { - return nil, status.Errorf(codes.Unimplemented, "Unimplemented") + if e, ok := m.chassisInventory[*lookup]; ok { + return e, nil + } + + return nil, status.Errorf(codes.NotFound, "chassis %+v not found in inventory", *lookup) +} + +func (m *InMemoryEntityManager) GetBootstrapData(c *bootz.ControlCard) (*bootz.BootstrapDataResponse, error) { + // First check if we are expecting this control card. + if c.SerialNumber == "" { + return nil, status.Errorf(codes.InvalidArgument, "no serial number provided") + } + if _, ok := m.controlCardStatuses[c.GetSerialNumber()]; !ok { + return nil, status.Errorf(codes.NotFound, "control card %v not found in inventory", c.GetSerialNumber()) + } + // Construct the response. This emulator hardcodes these values but a real Bootz server would not. + // TODO: Populate these placeholders with realistic ones. + return &bootz.BootstrapDataResponse{ + SerialNum: c.SerialNumber, + IntendedImage: &bootz.SoftwareImage{ + Name: "Default Image", + Version: "1.0", + Url: "https://path/to/image", + OsImageHash: "ABCDEF", + HashAlgorithm: "SHA256", + }, + BootPasswordHash: "ABCD123", + ServerTrustCert: "FakeTLSCert", + BootConfig: &bootz.BootConfig{ + VendorConfig: []byte("Vendor Config"), + OcConfig: []byte("OC Config"), + }, + Credentials: &bootz.Credentials{}, + // TODO: Populate pathz, authz and certificates. + }, nil +} + +func (m *InMemoryEntityManager) SetStatus(req *bootz.ReportStatusRequest) error { + if len(req.GetStates()) == 0 { + return status.Errorf(codes.InvalidArgument, "no control card states provided") + } + log.Infof("Bootstrap Status: %v: Status message: %v", req.GetStatus(), req.GetStatusMessage()) + + m.mu.Lock() + defer m.mu.Unlock() + for _, c := range req.GetStates() { + previousStatus, ok := m.controlCardStatuses[c.GetSerialNumber()] + if !ok { + return status.Errorf(codes.NotFound, "control card %v not found in inventory", c.GetSerialNumber()) + } + log.Infof("control card %v changed status from %v to %v", c.GetSerialNumber(), previousStatus, c.GetStatus()) + m.controlCardStatuses[c.GetSerialNumber()] = c.GetStatus() + } + return nil +} + +// Sign unmarshals the SignedResponse bytes then generates a signature from its Ownership Certificate private key. +func (m *InMemoryEntityManager) Sign(resp *bootz.GetBootstrapDataResponse, serial string) error { + block, _ := pem.Decode([]byte(m.artifacts.OC.Key)) + if block == nil { + return status.Errorf(codes.Internal, "unable to decode OC private key") + } + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return err + } + if resp.GetSignedResponse() == nil { + return status.Errorf(codes.InvalidArgument, "empty signed response") + } + signedResponseBytes, err := proto.Marshal(resp.GetSignedResponse()) + if err != nil { + return err + } + hashed := sha256.Sum256(signedResponseBytes) + // TODO: Add support for EC keys too. + sig, err := rsa.SignPKCS1v15(nil, priv, crypto.SHA256, hashed[:]) + if err != nil { + return err + } + resp.ResponseSignature = base64.StdEncoding.EncodeToString(sig) + // Populate the OV + ov, err := m.FetchOwnershipVoucher(serial) + if err != nil { + return err + } + resp.OwnershipVoucher = []byte(ov) + // Populate the OC + resp.OwnershipCertificate = []byte(m.artifacts.OC.Cert) + return nil } -// GetBootstrapData returns the Bootstrap data for the provided control card. -func (m *InMemoryEntityManager) GetBootstrapData(*bootz.ControlCard) (*bootz.BootstrapDataResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "Unimplemented") +// FetchOwnershipVoucher retrieves the ownership voucher for a control card +func (m *InMemoryEntityManager) FetchOwnershipVoucher(serial string) (string, error) { + if ov, ok := m.artifacts.OV[serial]; ok { + return ov, nil + } + return "", status.Errorf(codes.NotFound, "OV for serial %v not found", serial) } -// SetStatus returns the current status based on the status request. -func (m *InMemoryEntityManager) SetStatus(*bootz.ReportStatusRequest) error { - return status.Errorf(codes.Unimplemented, "Unimplemented") +// AddControlCard adds a new control card to the entity manager. +func (m *InMemoryEntityManager) AddControlCard(serial string) *InMemoryEntityManager { + m.mu.Lock() + defer m.mu.Unlock() + m.controlCardStatuses[serial] = bootz.ControlCardState_CONTROL_CARD_STATUS_UNSPECIFIED + return m } -// Sign populates the signing fields of the provided Bootstrap data response. -// If fields are set they will be overwritten. -func (m *InMemoryEntityManager) Sign(*bootz.GetBootstrapDataResponse) error { - return status.Errorf(codes.Unimplemented, "Unimplemented") +// AddChassis adds a new chassis to the entity manager. +func (m *InMemoryEntityManager) AddChassis(bootMode bootz.BootMode, manufacturer string, serial string) *InMemoryEntityManager { + m.mu.Lock() + defer m.mu.Unlock() + l := service.EntityLookup{ + Manufacturer: manufacturer, + SerialNumber: serial, + } + m.chassisInventory[l] = &service.ChassisEntity{ + BootMode: bootMode, + } + return m } // New returns a new in-memory entity manager. -func New() *InMemoryEntityManager { - return &InMemoryEntityManager{} +func New(artifacts *service.SecurityArtifacts) *InMemoryEntityManager { + return &InMemoryEntityManager{ + artifacts: artifacts, + chassisInventory: make(map[service.EntityLookup]*service.ChassisEntity), + controlCardStatuses: make(map[string]bootz.ControlCardState_ControlCardStatus), + } } diff --git a/server/entitymanager/entitymanager_test.go b/server/entitymanager/entitymanager_test.go new file mode 100644 index 0000000..d0619f4 --- /dev/null +++ b/server/entitymanager/entitymanager_test.go @@ -0,0 +1,281 @@ +package entitymanager + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/openconfig/bootz/proto/bootz" + "github.com/openconfig/bootz/server/service" + "google.golang.org/protobuf/proto" +) + +func TestFetchOwnershipVoucher(t *testing.T) { + tests := []struct { + desc string + serial string + want string + wantErr bool + }{{ + desc: "Missing OV", + serial: "123B", + wantErr: true, + }, { + desc: "Found OV", + serial: "123A", + want: "test_ov", + wantErr: false, + }} + + artifacts := &service.SecurityArtifacts{ + OV: service.OVList{"123A": "test_ov"}, + } + em := New(artifacts) + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + got, err := em.FetchOwnershipVoucher(test.serial) + if (err != nil) != test.wantErr { + t.Fatalf("FetchOwnershipVoucher(%v) err = %v, want %v", test.serial, err, test.wantErr) + } + if !cmp.Equal(got, test.want) { + t.Errorf("FetchOwnershipVoucher(%v) got %v, want %v", test.serial, got, test.want) + } + }) + } +} + +func TestResolveChassis(t *testing.T) { + tests := []struct { + desc string + input *service.EntityLookup + want *service.ChassisEntity + wantErr bool + }{{ + desc: "Default device", + input: &service.EntityLookup{ + SerialNumber: "123", + Manufacturer: "Cisco", + }, + want: &service.ChassisEntity{ + BootMode: bootz.BootMode_BOOT_MODE_SECURE, + }, + }, { + desc: "Chassis Not Found", + input: &service.EntityLookup{ + SerialNumber: "456", + Manufacturer: "Cisco", + }, + want: nil, + wantErr: true, + }, + } + + em := New(nil).AddChassis(bootz.BootMode_BOOT_MODE_SECURE, "Cisco", "123") + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + got, err := em.ResolveChassis(test.input) + if (err != nil) != test.wantErr { + t.Fatalf("ResolveChassis(%v) err = %v, want %v", test.input, err, test.wantErr) + } + if !cmp.Equal(got, test.want) { + t.Errorf("ResolveChassis(%v) got %v, want %v", test.input, got, test.want) + } + }) + } +} + +func TestSign(t *testing.T) { + tests := []struct { + desc string + serial string + resp *bootz.GetBootstrapDataResponse + wantOV string + wantOC string + wantErr bool + }{{ + desc: "Success", + serial: "123A", + resp: &bootz.GetBootstrapDataResponse{ + SignedResponse: &bootz.BootstrapDataSigned{ + Responses: []*bootz.BootstrapDataResponse{ + {SerialNum: "123A"}, + }, + }, + }, + wantOV: "test_ov", + wantOC: "test_oc", + wantErr: false, + }, { + desc: "Empty response", + resp: &bootz.GetBootstrapDataResponse{}, + wantErr: true, + }, + } + + priv, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + t.Fatal(err) + } + privPEM := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(priv), + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + artifacts := &service.SecurityArtifacts{ + OV: service.OVList{test.serial: test.wantOV}, + OC: &service.KeyPair{ + Cert: test.wantOC, + Key: string(pem.EncodeToMemory(privPEM)), + }, + } + em := New(artifacts) + + err := em.Sign(test.resp, test.serial) + if err != nil { + if test.wantErr { + t.Skip() + } + t.Errorf("Sign() err = %v, want %v", err, test.wantErr) + } + signedResponseBytes, err := proto.Marshal(test.resp.GetSignedResponse()) + if err != nil { + t.Fatal(err) + } + hashed := sha256.Sum256(signedResponseBytes) + sigDecoded, err := base64.StdEncoding.DecodeString(test.resp.GetResponseSignature()) + if err != nil { + t.Fatal(err) + } + err = rsa.VerifyPKCS1v15(&priv.PublicKey, crypto.SHA256, hashed[:], sigDecoded) + if err != nil { + t.Errorf("Sign() err == %v, want %v", err, test.wantErr) + } + if gotOV, wantOV := string(test.resp.GetOwnershipVoucher()), test.wantOV; gotOV != wantOV { + t.Errorf("Sign() ov = %v, want %v", gotOV, wantOV) + } + if gotOC, wantOC := string(test.resp.GetOwnershipCertificate()), test.wantOC; gotOC != wantOC { + t.Errorf("Sign() oc = %v, want %v", gotOC, wantOC) + } + }) + } +} + +func TestSetStatus(t *testing.T) { + tests := []struct { + desc string + input *bootz.ReportStatusRequest + wantErr bool + }{{ + desc: "No control card states", + input: &bootz.ReportStatusRequest{ + Status: bootz.ReportStatusRequest_BOOTSTRAP_STATUS_SUCCESS, + StatusMessage: "Bootstrap status succeeded", + }, + wantErr: true, + }, { + desc: "Control card initialized", + input: &bootz.ReportStatusRequest{ + Status: bootz.ReportStatusRequest_BOOTSTRAP_STATUS_SUCCESS, + StatusMessage: "Bootstrap status succeeded", + States: []*bootz.ControlCardState{ + { + SerialNumber: "123A", + Status: *bootz.ControlCardState_CONTROL_CARD_STATUS_INITIALIZED.Enum(), + }, + }, + }, + wantErr: false, + }, { + desc: "Unknown control card", + input: &bootz.ReportStatusRequest{ + Status: bootz.ReportStatusRequest_BOOTSTRAP_STATUS_SUCCESS, + StatusMessage: "Bootstrap status succeeded", + States: []*bootz.ControlCardState{ + { + SerialNumber: "123C", + Status: *bootz.ControlCardState_CONTROL_CARD_STATUS_INITIALIZED.Enum(), + }, + }, + }, + wantErr: true, + }, + } + + em := New(nil).AddChassis(bootz.BootMode_BOOT_MODE_SECURE, "Cisco", "123").AddControlCard("123A") + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + err := em.SetStatus(test.input) + if (err != nil) != test.wantErr { + t.Errorf("SetStatus(%v) err = %v, want %v", test.input, err, test.wantErr) + } + }) + } +} + +func TestGetBootstrapData(t *testing.T) { + tests := []struct { + desc string + input *bootz.ControlCard + want *bootz.BootstrapDataResponse + wantErr bool + }{{ + desc: "No serial number", + input: &bootz.ControlCard{}, + wantErr: true, + }, { + desc: "Control card not found", + input: &bootz.ControlCard{ + SerialNumber: "456A", + }, + wantErr: true, + }, { + desc: "Successful bootstrap", + input: &bootz.ControlCard{ + SerialNumber: "123A", + }, + want: &bootz.BootstrapDataResponse{ + SerialNum: "123A", + IntendedImage: &bootz.SoftwareImage{ + Name: "Default Image", + Version: "1.0", + Url: "https://path/to/image", + OsImageHash: "ABCDEF", + HashAlgorithm: "SHA256", + }, + BootPasswordHash: "ABCD123", + ServerTrustCert: "FakeTLSCert", + BootConfig: &bootz.BootConfig{ + VendorConfig: []byte("Vendor Config"), + OcConfig: []byte("OC Config"), + }, + Credentials: &bootz.Credentials{}, + }, + wantErr: false, + }, + } + + em := New(nil).AddChassis(bootz.BootMode_BOOT_MODE_SECURE, "Cisco", "123").AddControlCard("123A") + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + got, err := em.GetBootstrapData(test.input) + if (err != nil) != test.wantErr { + t.Errorf("GetBootstrapData(%v) err = %v, want %v", test.input, err, test.wantErr) + } + if !proto.Equal(got, test.want) { + t.Errorf("GetBootstrapData(%v) got %v, want %v", test.input, got, test.want) + } + }) + } +} diff --git a/server/server.go b/server/server.go index 8183e9c..b1d4b10 100644 --- a/server/server.go +++ b/server/server.go @@ -6,31 +6,136 @@ package main import ( + "crypto/tls" + "crypto/x509" "flag" "fmt" "net" + "os" + "path/filepath" + "strings" "github.com/openconfig/bootz/proto/bootz" "github.com/openconfig/bootz/server/entitymanager" "github.com/openconfig/bootz/server/service" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" log "github.com/golang/glog" ) var ( - port = flag.String("port", "", "The port to start the Bootz server on localhost") + port = flag.String("port", "", "The port to start the Bootz server on localhost") + artifactDirectory = flag.String("artifact_dir", "../testdata/", "The relative directory to look into for certificates, private keys and OVs.") ) +// readKeyPair reads the cert/key pair from the specified artifacts directory. +// Certs must have the format {name}_pub.pem and keys must have the format {name}_priv.pem +func readKeypair(name string) (*service.KeyPair, error) { + cert, err := os.ReadFile(filepath.Join(*artifactDirectory, fmt.Sprintf("%v_pub.pem", name))) + if err != nil { + return nil, fmt.Errorf("unable to read %v cert: %v", name, err) + } + key, err := os.ReadFile(filepath.Join(*artifactDirectory, fmt.Sprintf("%v_priv.pem", name))) + if err != nil { + return nil, fmt.Errorf("unable to read %v key: %v", name, err) + } + return &service.KeyPair{ + Cert: string(cert), + Key: string(key), + }, nil +} + +// readOVs discovers and reads all available OVs in the artifacts directory. +func readOVs() (service.OVList, error) { + ovs := make(service.OVList) + files, err := os.ReadDir(*artifactDirectory) + if err != nil { + return nil, fmt.Errorf("unable to list files in artifact directory: %v", err) + } + for _, f := range files { + if strings.HasPrefix(f.Name(), "ov") { + bytes, err := os.ReadFile(filepath.Join(*artifactDirectory, f.Name())) + if err != nil { + return nil, err + } + trimmed := strings.TrimPrefix(f.Name(), "ov_") + trimmed = strings.TrimSuffix(trimmed, ".txt") + ovs[trimmed] = string(bytes) + } + } + if len(ovs) == 0 { + return nil, fmt.Errorf("found no OVs in artifacts directory") + } + return ovs, err +} + +// generateServerTlsCert creates a new TLS keypair from the PDC. +func generateServerTlsCert(pdc *service.KeyPair) (*tls.Certificate, error) { + tlsCert, err := tls.X509KeyPair([]byte(pdc.Cert), []byte(pdc.Key)) + if err != nil { + return nil, fmt.Errorf("unable to generate Server TLS Certificate from PDC %v", err) + } + return &tlsCert, err +} + +// parseSecurityArtifacts reads from the specified directory to find the required keypairs and ownership vouchers. +func parseSecurityArtifacts() (*service.SecurityArtifacts, error) { + oc, err := readKeypair("oc") + if err != nil { + return nil, err + } + pdc, err := readKeypair("pdc") + if err != nil { + return nil, err + } + vendorCA, err := readKeypair("vendorca") + if err != nil { + return nil, err + } + ovs, err := readOVs() + if err != nil { + return nil, err + } + tlsCert, err := generateServerTlsCert(pdc) + if err != nil { + return nil, err + } + return &service.SecurityArtifacts{ + OC: oc, + PDC: pdc, + VendorCA: vendorCA, + OV: ovs, + TLSKeypair: tlsCert, + }, nil +} + func main() { flag.Parse() if *port == "" { log.Exitf("no port selected. specify with the -port flag") } - em := entitymanager.New() + if *artifactDirectory == "" { + log.Exitf("no artifact directory specified") + } + sa, err := parseSecurityArtifacts() + if err != nil { + log.Exit(err) + } + em := entitymanager.New(sa) + em.AddChassis(bootz.BootMode_BOOT_MODE_SECURE, "Cisco", "123").AddControlCard("123A").AddControlCard("123B") c := service.New(em) - s := grpc.NewServer() + + trustBundle := x509.NewCertPool() + if !trustBundle.AppendCertsFromPEM([]byte(sa.PDC.Cert)) { + log.Exitf("unable to add PDC cert to trust pool") + } + tls := &tls.Config{ + Certificates: []tls.Certificate{*sa.TLSKeypair}, + RootCAs: trustBundle, + } + s := grpc.NewServer(grpc.Creds(credentials.NewTLS(tls))) lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%v", *port)) if err != nil { diff --git a/server/service/service.go b/server/service/service.go index 8d26b63..94d90cc 100644 --- a/server/service/service.go +++ b/server/service/service.go @@ -2,6 +2,7 @@ package service import ( "context" + "crypto/tls" "github.com/openconfig/bootz/proto/bootz" "github.com/openconfig/gnmi/errlist" @@ -9,6 +10,32 @@ import ( "google.golang.org/grpc/status" ) +// OVList is a mapping of control card serial number to ownership voucher. +type OVList map[string]string + +// KeyPair is a struct containing PEM-encoded certificates and private keys. +type KeyPair struct { + Cert string + Key string +} + +// SecurityArtifacts contains all KeyPairs and OVs needed for the Bootz Server. +// Currently, RSA is the only encryption standard supported by these artifacts. +type SecurityArtifacts struct { + // The Ownership Certificate is an x509 certificate/private key pair signed by the PDC. + // The certificate is presented to the device during bootstrapping and is used to validate the Ownership Voucher. + OC *KeyPair + // The Pinned Domain Certificate is an x509 certificate/private key pair which acts as a certificate authority on the owner's side. + // This certificate is included in OVs and is also used to generate a server TLS Cert in this implementation. + PDC *KeyPair + // The Vendor CA represents a certificate authority on the vendor side. This CA signs Ownership Vouchers which are verified by the device. + VendorCA *KeyPair + // Ownership Vouchers are a list of PKCS7 messages signed by the Vendor CA. There is one per control card. + OV OVList + // The TLSKeypair is a TLS certificate used to secure connections between device and server. It is derived from the Pinned Domain Cert. + TLSKeypair *tls.Certificate +} + // EntityLookup provides a way to resolve chassis and control cards // in the EntityManager. type EntityLookup struct { @@ -26,7 +53,8 @@ type EntityManager interface { ResolveChassis(*EntityLookup) (*ChassisEntity, error) GetBootstrapData(*bootz.ControlCard) (*bootz.BootstrapDataResponse, error) SetStatus(*bootz.ReportStatusRequest) error - Sign(*bootz.GetBootstrapDataResponse) error + Sign(*bootz.GetBootstrapDataResponse, string) error + FetchOwnershipVoucher(string) (string, error) } type Service struct { @@ -34,7 +62,7 @@ type Service struct { em EntityManager } -func (s *Service) GetBootstrapRequest(ctx context.Context, req *bootz.GetBootstrapDataRequest) (*bootz.GetBootstrapDataResponse, error) { +func (s *Service) GetBootstrapData(ctx context.Context, req *bootz.GetBootstrapDataRequest) (*bootz.GetBootstrapDataResponse, error) { if len(req.ChassisDescriptor.ControlCards) == 0 { return nil, status.Errorf(codes.InvalidArgument, "request must include at least one control card") } @@ -68,6 +96,7 @@ func (s *Service) GetBootstrapRequest(ctx context.Context, req *bootz.GetBootstr if errs.Err() != nil { return nil, errs.Err() } + resp := &bootz.GetBootstrapDataResponse{ SignedResponse: &bootz.BootstrapDataSigned{ Responses: responses, @@ -75,7 +104,8 @@ func (s *Service) GetBootstrapRequest(ctx context.Context, req *bootz.GetBootstr } // Sign the response if Nonce is provided. if req.Nonce != "" { - if err := s.em.Sign(resp); err != nil { + resp.SignedResponse.Nonce = req.Nonce + if err := s.em.Sign(resp, req.GetControlCardState().GetSerialNumber()); err != nil { return nil, status.Errorf(codes.Internal, "failed to sign bootz response") } } @@ -83,7 +113,7 @@ func (s *Service) GetBootstrapRequest(ctx context.Context, req *bootz.GetBootstr } func (s *Service) ReportStatus(ctx context.Context, req *bootz.ReportStatusRequest) (*bootz.EmptyResponse, error) { - return nil, s.em.SetStatus(req) + return &bootz.EmptyResponse{}, s.em.SetStatus(req) } // Public API for allowing the device configuration to be set for each device the diff --git a/testdata/README.md b/testdata/README.md index 280afa6..e4f12b5 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -28,7 +28,8 @@ real world, a reliable CA chain should be used instead. ### pdc_{pub|priv}.pem This is an x509 certificate/RSA keypair that represents the owner's Pinned -Domain Cert. +Domain Cert. This keypair is also used to create a TLS certificate for a +secure connection. Note: In this example these certifcates are self-signed for convenience. In the real world, a reliable CA chain should be used instead. diff --git a/testdata/ca.pem b/testdata/ca.pem deleted file mode 100644 index 4703e6a..0000000 --- a/testdata/ca.pem +++ /dev/null @@ -1,32 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFeDCCA2CgAwIBAgITKAAAABSVcgp8E6vpUAAAAAAAFDANBgkqhkiG9w0BAQsF -ADA6MTgwNgYDVQQDEy9BcmlzdGEgTmV0d29ya3MgSW50ZXJuYWwgSVQgUm9vdCBD -ZXJ0IEF1dGhvcml0eTAeFw0yMjA0MDcyMTUyMTNaFw0zNzA0MDcyMjAyMTNaMIGv -MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEUMBIGA1UEBxMLU2Fu -dGEgQ2xhcmExHTAbBgNVBAoTFEFyaXN0YSBOZXR3b3JrcyBJbmMuMR8wHQYDVQQL -ExZJbmZvcm1hdGlvbiBUZWNobm9sb2d5MTUwMwYDVQQDEyxBcmlzdGEgTmV0d29y -a3MgWlRQIFNpZ25lciBJc3N1aW5nIEF1dGhvcml0eTCBmzAQBgcqhkjOPQIBBgUr -gQQAIwOBhgAEAMTxjD+CQTUrDRu6y+9GjoiFqL9iJrq6oxhx2OP/318JgrOY+i0W -8ai4L/KIh3GEa3T41ezEqyNAABt+Uw6PFNlvAI6B9YdO1dP88696wfzCg2cPr+FH -v93a5aZlSYdhxe5ZUliQx6tT71CMJrUZJjsTYkhr6YUpZjUXW4MBG+7D8QeBo4IB -hzCCAYMwHQYDVR0OBBYEFGAPOuSq2hZHcPxzVgqfmtE8fWAuMB8GA1UdIwQYMBaA -FC3v2ubvzxx3Ub9aAqkozZQAreHKMHQGA1UdHwRtMGswaaBnoGWGY2ZpbGU6Ly8v -L1dJTi0zRzZJRzAySzFSMC9DZXJ0RW5yb2xsL0FyaXN0YSUyME5ldHdvcmtzJTIw -SW50ZXJuYWwlMjBJVCUyMFJvb3QlMjBDZXJ0JTIwQXV0aG9yaXR5LmNybDCBkQYI -KwYBBQUHAQEEgYQwgYEwfwYIKwYBBQUHMAKGc2ZpbGU6Ly8vL1dJTi0zRzZJRzAy -SzFSMC9DZXJ0RW5yb2xsL1dJTi0zRzZJRzAySzFSMF9BcmlzdGElMjBOZXR3b3Jr -cyUyMEludGVybmFsJTIwSVQlMjBSb290JTIwQ2VydCUyMEF1dGhvcml0eS5jcnQw -GQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDwYDVR0TAQH/BAUwAwEB/zALBgNV -HQ8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBAABN+601/TPZY7YGolaRI2hbgpdP -adQZ1psrR5nqEplJkHcjLEqhXFD4b4NVerTSuIxiUBI4scEy6/R7WRUEWgLQx90y -Vd9YFayB3U2ilhPtbflc2p6SabQcjldWp9Y5AAl2vDaki8UrrizxuPeaxcroh1iI -aj9437MaxTCSm42dxmCExmylL+Q3oZ88ZRfaFoCa0S2XzQxxBbznpzLKQrgMG9fQ -b0b7FRzTOnYWcy57Mpn8/ZqFX/ZymUv3kb2iqlP5IFR0/uxeYcjKxHwjnnlX56RS -s5MJD25Lr4OgJPFCSi5TiQUyoLGWfqFvrdyDbpLEj8lnFkUuloR5RVJrRlfWQ2fO -RvYjcUqlQ1AlFtPh0PCR7SyF94imMdKGx1pimOChsgDBC1ry+XcctLgvTw9kQzcx -E2u6H9mwTbHp4iZaI+OXJkQQGSE+x84hdRe4yXldH7fTFbMOuvFBM5QzdBnlx+C/ -Uf5Tc0UvaKT+aCOjhQ1cl09J9ruyPCukRnDKDZS+jj0b5cvB2VP+aKo6R3o1Gnkx -vRDZLZrPbqxAiDWgYzZdDP6AEk2roe5/T5HlDEpLxtLVkzmFT704Z7pFn8utWIvA -FFJAm3CKlnNrN6VYuhiv6uOMuL9+6qjBBN+rGmr9fNPcQWepKKNuReQ+48HCmxKl -T8meIZH2lwZ+DZbk ------END CERTIFICATE----- \ No newline at end of file