Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Entitymanager and populate security artifacts #12

Merged
merged 14 commits into from
Aug 23, 2023
20 changes: 12 additions & 8 deletions client/README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
22 changes: 14 additions & 8 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
Expand All @@ -21,17 +22,18 @@ 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"
)

// Represents a 128 bit nonce.
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 {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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() {
Expand Down Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions server/README.md
Original file line number Diff line number Diff line change
@@ -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.
153 changes: 139 additions & 14 deletions server/entitymanager/entitymanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
Loading