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

Add a simple fake client to Bootz #8

Merged
merged 8 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
load("@bazel_gazelle//:def.bzl", "gazelle")

# gazelle:prefix github.com/openconfig/gnsi
# gazelle:prefix github.com/openconfig/bootz
gazelle(name = "gazelle")

18 changes: 18 additions & 0 deletions client/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_binary")

go_library(
name = "client_lib",
srcs = ["client.go"],
importpath = "github.com/openconfig/bootz/client",
visibility = ["//visibility:public"],
deps = [
"@com_github_golang_glog//:go_default_library",
"@org_golang_google_grpc//:go_default_library",
],
)

go_binary(
name = "client",
embed = [":client_lib"],
visibility = ["//visibility:public"],
)
15 changes: 15 additions & 0 deletions client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Bootz Client Emulator

The code located in this directory is intended to emulator 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

### Flags

* `port`: The port to listen to the Bootz Server on localhost.
* `insecure_boot`: Whether to set start the emulated client in an insecure boot mode, in which ownership voucher and certificates aren't checked.
* `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.
Binary file added client/client
Binary file not shown.
299 changes: 294 additions & 5 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,308 @@
package client
package main

import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
"os"
"strings"
"time"

log "github.com/golang/glog"

"github.com/openconfig/bootz/proto/bootz"
"go.mozilla.org/pkcs7"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/proto"
)

type Client struct {
bootz.BootstrapClient
// 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.")
)

type OwnershipVoucher struct {
OV OwnershipVoucherInner `json:"ietf-voucher:voucher"`
}

// Defines the Ownership Voucher format. See https://www.rfc-editor.org/rfc/rfc8366.html.
type OwnershipVoucherInner struct {
CreatedOn string `json:"created-on"`
ExpiresOn string `json:"expires-on"`
SerialNumber string `json:"serial-number"`
Assertion string `json:"assertion"`
PinnedDomainCert string `json:"pinned-domain-cert"`
DomainCertRevocationChecks bool `json:"domain-cert-revocation-checks"`
}

// pemEncodeCert adds the correct PEM headers and footers to a raw certificate block.
func pemEncodeCert(contents string) string {
gmacf marked this conversation as resolved.
Show resolved Hide resolved
return strings.Join([]string{"-----BEGIN CERTIFICATE-----", contents, "-----END CERTIFICATE-----"}, "\n")
}

func (c *Client) GetBootstrapData(ctx context.Context, req *bootz.GetBootstrapDataRequest, opts ...grpc.CallOption) (*bootz.GetBootstrapDataResponse, error) {
// validateArtifacts checks the signed artifacts in a GetBootstrapDataResponse. Specifically, it:
// - Checks that the OV in the response is signed by the manufacturer
// - Checks that the serial number in the OV matches the one in the original request.
// - Verififies that the Ownership Certificate is in the chain of signers of the Pinned Domain Cert.
func validateArtifacts(serialNumber string, resp *bootz.GetBootstrapDataResponse, rootCA []byte) error {
ov64 := resp.GetOwnershipVoucher()
if len(ov64) == 0 {
return fmt.Errorf("received empty ownership voucher from server")
}

oc := resp.GetOwnershipCertificate()
if len(oc) == 0 {
return fmt.Errorf("received empty ownership certificate from server")
}
// Decode the ownership voucher
ov, err := base64.StdEncoding.DecodeString(string(ov64))
if err != nil {
return err
}

// Parse the PKCS7 message
p7, err := pkcs7.Parse(ov)
if err != nil {
return err
}
// Unmarshal the ownership voucher into a struct.
parsedOV := OwnershipVoucher{}
err = json.Unmarshal(p7.Content, &parsedOV)
if err != nil {
return err
}

// Create a CA pool for the device to validate that the vendor has signed this OV.
vendorCAPool := x509.NewCertPool()
if !vendorCAPool.AppendCertsFromPEM(rootCA) {
return fmt.Errorf("unable to add vendor root CA to pool")
}

// Verify the ownership voucher with this CA.
err = p7.VerifyWithChain(vendorCAPool)
if err != nil {
return err
}

log.Infof("Validated ownership voucher signed by vendor")

// Verify the serial number for this OV
if parsedOV.OV.SerialNumber != serialNumber {
return fmt.Errorf("serial number from OV does not match request")
}

pdCPEM := pemEncodeCert(parsedOV.OV.PinnedDomainCert)

// Create a new pool with this PDC.
pdcPool := x509.NewCertPool()
if !pdcPool.AppendCertsFromPEM([]byte(pdCPEM)) {
return err
}

// Parse the Ownership Certificate.
ocCert, err := certFromPemBlock(oc)
if err != nil {
return fmt.Errorf("failed to parse certificate: %v", err)
}

// Verify that the OC is signed by the PDC.
opts := x509.VerifyOptions{
Roots: pdcPool,
Intermediates: x509.NewCertPool(),
}
if _, err := ocCert.Verify(opts); err != nil {
return err
}
log.Infof("Validated ownership certificate with OV PDC\n")

// Validate the response signature.
signedResponseBytes, err := proto.Marshal(resp.GetSignedResponse())
if err != nil {
return err
}
hashed := sha256.Sum256(signedResponseBytes)

// 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()))
if err != nil {
return fmt.Errorf("signature not verified: %v", err)
}
default:
return fmt.Errorf("unsupported public key type: %T", pub)
}
log.Infof("Verified SignedResponse signature")

return nil
}
func (c *Client) ReportStatus(ctx context.Context, req *bootz.ReportStatusRequest, opts ...grpc.CallOption) (*bootz.EmptyResponse, error) {

func certFromPemBlock(data []byte) (*x509.Certificate, error) {
block, _ := pem.Decode(data)
if block == nil {
return nil, fmt.Errorf("failed to parse certificate PEM")
}
return x509.ParseCertificate(block.Bytes)
}

// generateNonce() generates a fixed-length nonce.
func generateNonce() (string, error) {
b := make([]byte, nonceLength)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return string(b), nil
}

func main() {
ctx := context.Background()
flag.Parse()

if *rootCA == "" {
log.Exitf("No root CA certificate file specified")
}
rootCABytes, err := os.ReadFile(*rootCA)
if err != nil {
log.Exitf("Error opening Root CA file: %v", err)
}
// Verify the Root CA cert is valid.
caCert, err := certFromPemBlock(rootCABytes)
if err != nil {
log.Exitf("Error parsing CA cert")
}
log.Infof("Loaded Root CA certificate: %v", string(caCert.Subject.CommonName))

// Construct the fake device.
// TODO: Allow these values to be set e.g. via a flag.
chassis := bootz.ChassisDescriptor{
Manufacturer: "Cisco",
SerialNumber: "123",
ControlCards: []*bootz.ControlCard{
{
SerialNumber: "123A",
Slot: 1,
PartNumber: "123A",
},
{
SerialNumber: "123B",
Slot: 2,
PartNumber: "123B",
},
gmacf marked this conversation as resolved.
Show resolved Hide resolved
},
}

log.Infof("%v chassis %v starting with SecureOnly = %v", chassis.Manufacturer, chassis.SerialNumber, !*insecureBoot)

// 1. DHCP Discovery of Bootstrap Server
// This step emulates the retrieval of the bootz server IP
// address from a DHCP server. In this case we always connect to localhost.

if *port == "" {
log.Exitf("No port provided.")
}
bootzAddress := fmt.Sprintf("localhost:%v", *port)
log.Infof("Connecting to bootz server at address %q", bootzAddress)

// 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()))
if err != nil {
log.Exitf("Unable to connect to Bootstrap Server: %v", err)
}
defer conn.Close()
c := bootz.NewBootstrapClient(conn)
log.Infof("Connected to bootz server")

// This is the active control card making the bootz request.
activeControlCard := chassis.ControlCards[0]

nonce := ""
if !*insecureBoot {
// Generate a nonce that the Bootz server will use to sign the response.
nonce, err = generateNonce()
if err != nil {
log.Exitf("Error generating nonce: %v", err)
}
}

req := &bootz.GetBootstrapDataRequest{
ChassisDescriptor: &chassis,
// This is the active control card, e.g. the one making the bootz request.
ControlCardState: &bootz.ControlCardState{
SerialNumber: activeControlCard.GetSerialNumber(),
Status: bootz.ControlCardState_CONTROL_CARD_STATUS_NOT_INITIALIZED,
},
Nonce: nonce,
}

// Get bootstrapping data from Bootz server
// TODO: Extract and parse response.
log.Infof("Requesting Bootstrap Data from Bootz server")
resp, err := c.GetBootstrapData(ctx, req)
if err != nil {
log.Exitf("Error calling GetBootstrapData: %v", err)
}

// Only check OC, OV and response signature if SecureOnly is set.
if !*insecureBoot {
if err := validateArtifacts(activeControlCard.GetSerialNumber(), resp, rootCABytes); err != nil {
log.Exitf("Error validating signed data: %v", err)
}
}

signedResp := resp.GetSignedResponse()
if !*insecureBoot && signedResp.GetNonce() != nonce {
log.Exitf("GetBootstrapDataResponse nonce does not match")
}

// TODO: Verify the hash of the intended image.
// Simply print out the received configs we get. This section should actually contain the logic to verify and install the images and config.
for _, data := range signedResp.GetResponses() {
log.Infof("Received config for control card %v", data.GetSerialNum())
log.Infof("Downloading image %+v...", data.GetIntendedImage())
time.Sleep(time.Second * 5)
log.Infof("Done")
log.Infof("Installing boot config %+v...", data.GetBootConfig())
time.Sleep(time.Second * 5)
log.Infof("Done")
}

// 6. ReportProgress
log.Infof("Sending Status Report")
statusReq := &bootz.ReportStatusRequest{
Status: bootz.ReportStatusRequest_BOOTSTRAP_STATUS_SUCCESS,
StatusMessage: "Bootstrap Success",
States: []*bootz.ControlCardState{
{
Status: bootz.ControlCardState_CONTROL_CARD_STATUS_INITIALIZED,
SerialNumber: chassis.GetControlCards()[0].GetSerialNumber(),
},
{
Status: bootz.ControlCardState_CONTROL_CARD_STATUS_INITIALIZED,
SerialNumber: chassis.GetControlCards()[1].GetSerialNumber(),
},
},
}

_, err = c.ReportStatus(ctx, statusReq)
if err != nil {
log.Exitf("Error reporting status: %v", err)
}
log.Infof("Status report sent")
// At this point the device has minimal configuration and can receive further gRPC calls. After this, the TPM Enrollment and attestation occurs.
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ require (
)

require (
github.com/golang/glog v1.1.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/openconfig/gnmi v0.0.0-20220617175856-41246b1b3507 // indirect
github.com/openconfig/gnoi v0.0.0-20220809151450-6bddacd72ef8 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw=
github.com/golang/glog v1.1.1/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Expand Down Expand Up @@ -59,6 +61,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
Expand Down
Loading