metropolis: finish implementing TPMMode
This wraps up the implementation of TPMMode in ClusterConfiguration,
allowing operators to select whether nodes should or should not use
their TPM, based on local availability.
We keep the default behaviour to require a TPM, as we'd like to be
secure by default.
Change-Id: Ic8ac76d88ecc9de51f58ca99c92daede79d78ad7
Reviewed-on: https://review.monogon.dev/c/monogon/+/1495
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/node/core/cluster/cluster.go b/metropolis/node/core/cluster/cluster.go
index 8a1a3ae..529120b 100644
--- a/metropolis/node/core/cluster/cluster.go
+++ b/metropolis/node/core/cluster/cluster.go
@@ -73,6 +73,7 @@
}
close(m.oneway)
+ // Try sealed configuration first.
configuration, err := m.storageRoot.ESP.Metropolis.SealedConfiguration.Unseal()
if err == nil {
supervisor.Logger(ctx).Info("Sealed configuration present. attempting to join cluster")
@@ -84,13 +85,27 @@
if err != nil {
return fmt.Errorf("while reading cluster directory: %w", err)
}
- return m.join(ctx, configuration, cd)
+ return m.join(ctx, configuration, cd, true)
}
if !errors.Is(err, localstorage.ErrNoSealed) {
return fmt.Errorf("unexpected sealed config error: %w", err)
}
+ configuration, err = m.storageRoot.ESP.Metropolis.SealedConfiguration.ReadUnsafe()
+ if err == nil {
+ supervisor.Logger(ctx).Info("Non-sealed configuration present. attempting to join cluster")
+
+ // Read Cluster Directory and unmarshal it. Since the node is already
+ // registered with the cluster, the directory won't be bootstrapped from
+ // Node Parameters.
+ cd, err := m.storageRoot.ESP.Metropolis.ClusterDirectory.Unmarshal()
+ if err != nil {
+ return fmt.Errorf("while reading cluster directory: %w", err)
+ }
+ return m.join(ctx, configuration, cd, false)
+ }
+
supervisor.Logger(ctx).Info("No sealed configuration, looking for node parameters")
switch inner := m.nodeParams.Cluster.(type) {
diff --git a/metropolis/node/core/cluster/cluster_bootstrap.go b/metropolis/node/core/cluster/cluster_bootstrap.go
index 4f85fdc..78dea0b 100644
--- a/metropolis/node/core/cluster/cluster_bootstrap.go
+++ b/metropolis/node/core/cluster/cluster_bootstrap.go
@@ -45,14 +45,13 @@
return fmt.Errorf("invalid initial cluster configuration: %w", err)
}
- useTPM, err := cc.UseTPM(m.haveTPM)
+ tpmUsage, err := cc.NodeTPMUsage(m.haveTPM)
if err != nil {
return fmt.Errorf("cannot join cluster: %w", err)
}
supervisor.Logger(ctx).Infof("TPM: cluster TPM mode: %s", cc.TPMMode)
- supervisor.Logger(ctx).Infof("TPM: present in this node: %v", m.haveTPM)
- supervisor.Logger(ctx).Infof("TPM: used by this node: %v", useTPM)
+ supervisor.Logger(ctx).Infof("TPM: node TPM usage: %s", tpmUsage)
ownerKey := bootstrap.OwnerPublicKey
configuration := ppb.SealedConfiguration{}
@@ -89,7 +88,7 @@
}
supervisor.Logger(ctx).Infof("Bootstrapping: node public join key: %s", hex.EncodeToString([]byte(jpub)))
- m.roleServer.ProvideBootstrapData(priv, ownerKey, cuk, nuk, jpriv, cc)
+ m.roleServer.ProvideBootstrapData(priv, ownerKey, cuk, nuk, jpriv, cc, tpmUsage)
supervisor.Signal(ctx, supervisor.SignalHealthy)
supervisor.Signal(ctx, supervisor.SignalDone)
diff --git a/metropolis/node/core/cluster/cluster_join.go b/metropolis/node/core/cluster/cluster_join.go
index 2daea52..4fd6473 100644
--- a/metropolis/node/core/cluster/cluster_join.go
+++ b/metropolis/node/core/cluster/cluster_join.go
@@ -20,7 +20,7 @@
)
// join implements Join Flow of an already registered node.
-func (m *Manager) join(ctx context.Context, sc *ppb.SealedConfiguration, cd *cpb.ClusterDirectory) error {
+func (m *Manager) join(ctx context.Context, sc *ppb.SealedConfiguration, cd *cpb.ClusterDirectory, sealed bool) error {
// Generate a complete ED25519 Join Key based on the seed included in Sealed
// Configuration.
var jpriv ed25519.PrivateKey = sc.JoinKey
@@ -34,6 +34,7 @@
// Tell the user what we're doing.
hpkey := hex.EncodeToString(jpriv.Public().(ed25519.PublicKey))
supervisor.Logger(ctx).Infof("Joining an existing cluster.")
+ supervisor.Logger(ctx).Infof(" Using TPM-secured configuration: %v", sealed)
supervisor.Logger(ctx).Infof(" Node Join public key: %s", hpkey)
supervisor.Logger(ctx).Infof(" Directory:")
logClusterDirectory(ctx, cd)
@@ -75,7 +76,9 @@
bo := backoff.NewExponentialBackOff()
bo.MaxElapsedTime = 0
backoff.Retry(func() error {
- jr, err = cur.JoinNode(ctx, &ipb.JoinNodeRequest{})
+ jr, err = cur.JoinNode(ctx, &ipb.JoinNodeRequest{
+ UsingSealedConfiguration: sealed,
+ })
if err != nil {
supervisor.Logger(ctx).Warningf("Join failed: %v", err)
// This is never used.
diff --git a/metropolis/node/core/cluster/cluster_register.go b/metropolis/node/core/cluster/cluster_register.go
index 6ef9763..0096fa7 100644
--- a/metropolis/node/core/cluster/cluster_register.go
+++ b/metropolis/node/core/cluster/cluster_register.go
@@ -14,11 +14,12 @@
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
- ipb "source.monogon.dev/metropolis/node/core/curator/proto/api"
"source.monogon.dev/metropolis/node/core/identity"
"source.monogon.dev/metropolis/node/core/rpc"
"source.monogon.dev/metropolis/node/core/rpc/resolver"
"source.monogon.dev/metropolis/pkg/supervisor"
+
+ ipb "source.monogon.dev/metropolis/node/core/curator/proto/api"
apb "source.monogon.dev/metropolis/proto/api"
ppb "source.monogon.dev/metropolis/proto/private"
)
@@ -139,14 +140,18 @@
// logic into some sort of state machine where we can atomically make progress
// on each of the stages and get rid of the retry loops. The cluster enrolment
// code should let us do this quite easily.
- _, err = cur.RegisterNode(ctx, &ipb.RegisterNodeRequest{
+ res, err := cur.RegisterNode(ctx, &ipb.RegisterNodeRequest{
RegisterTicket: register.RegisterTicket,
JoinKey: jpub,
+ HaveLocalTpm: m.haveTPM,
})
if err != nil {
return fmt.Errorf("register call failed: %w", err)
}
+ supervisor.Logger(ctx).Infof("TPM: cluster TPM mode: %s", res.ClusterConfiguration.TpmMode)
+ supervisor.Logger(ctx).Infof("TPM: node TPM usage: %v", res.TpmUsage)
+
// Attempt to commit in a loop. This will succeed once the node is approved.
supervisor.Logger(ctx).Infof("Registering: success, attempting to commit...")
var certBytes, caCertBytes []byte
@@ -185,7 +190,7 @@
// Include the Cluster CA in Sealed Configuration.
sc.ClusterCa = register.CaCertificate
// Save Cluster CA, NUK and Join Credentials into Sealed Configuration.
- if err = m.storageRoot.ESP.Metropolis.SealedConfiguration.SealSecureBoot(&sc); err != nil {
+ if err = m.storageRoot.ESP.Metropolis.SealedConfiguration.SealSecureBoot(&sc, res.TpmUsage); err != nil {
return err
}
unix.Sync()
diff --git a/metropolis/node/core/curator/impl_leader_curator.go b/metropolis/node/core/curator/impl_leader_curator.go
index fe753f2..f698661 100644
--- a/metropolis/node/core/curator/impl_leader_curator.go
+++ b/metropolis/node/core/curator/impl_leader_curator.go
@@ -322,6 +322,17 @@
l.muNodes.Lock()
defer l.muNodes.Unlock()
+ cl, err := clusterLoad(ctx, l.leadership)
+ if err != nil {
+ return nil, err
+ }
+
+ // Figure out if node should be using TPM.
+ tpmUsage, err := cl.NodeTPMUsage(req.HaveLocalTpm)
+ if err != nil {
+ return nil, status.Errorf(codes.PermissionDenied, "%s", err)
+ }
+
// Check if there already is a node with this pubkey in the cluster.
id := identity.NodeID(pubkey)
node, err := nodeLoad(ctx, l.leadership, id)
@@ -346,14 +357,21 @@
// No node exists, create one.
node = &Node{
- pubkey: pubkey,
- jkey: req.JoinKey,
- state: cpb.NodeState_NODE_STATE_NEW,
+ pubkey: pubkey,
+ jkey: req.JoinKey,
+ state: cpb.NodeState_NODE_STATE_NEW,
+ tpmUsage: tpmUsage,
}
if err := nodeSave(ctx, l.leadership, node); err != nil {
return nil, err
}
- return &ipb.RegisterNodeResponse{}, nil
+
+ // Eat error, as we just deserialized this from a proto.
+ clusterConfig, _ := cl.proto()
+ return &ipb.RegisterNodeResponse{
+ ClusterConfiguration: clusterConfig,
+ TpmUsage: tpmUsage,
+ }, nil
}
func (l *leaderCurator) CommitNode(ctx context.Context, req *ipb.CommitNodeRequest) (*ipb.CommitNodeResponse, error) {
@@ -467,6 +485,29 @@
return nil, err
}
+ cl, err := clusterLoad(ctx, l.leadership)
+ if err != nil {
+ return nil, err
+ }
+
+ switch cl.TPMMode {
+ case cpb.ClusterConfiguration_TPM_MODE_REQUIRED:
+ if !req.UsingSealedConfiguration {
+ return nil, status.Errorf(codes.PermissionDenied, "cannot join this cluster with an unsealed configuration")
+ }
+ case cpb.ClusterConfiguration_TPM_MODE_DISABLED:
+ if req.UsingSealedConfiguration {
+ return nil, status.Errorf(codes.PermissionDenied, "cannot join this cluster with a sealed configuration")
+ }
+ }
+
+ if node.tpmUsage == cpb.NodeTPMUsage_NODE_TPM_PRESENT_AND_USED && !req.UsingSealedConfiguration {
+ return nil, status.Errorf(codes.PermissionDenied, "node registered with TPM, cannot join without one")
+ }
+ if node.tpmUsage != cpb.NodeTPMUsage_NODE_TPM_PRESENT_AND_USED && req.UsingSealedConfiguration {
+ return nil, status.Errorf(codes.PermissionDenied, "node registered without TPM, cannot join with one")
+ }
+
// Don't progress further unless the node is already UP.
if node.state != cpb.NodeState_NODE_STATE_UP {
return nil, status.Errorf(codes.FailedPrecondition, "node isn't UP, cannot join")
diff --git a/metropolis/node/core/curator/impl_leader_management.go b/metropolis/node/core/curator/impl_leader_management.go
index 461df02..eac4635 100644
--- a/metropolis/node/core/curator/impl_leader_management.go
+++ b/metropolis/node/core/curator/impl_leader_management.go
@@ -230,6 +230,7 @@
Roles: roles,
TimeSinceHeartbeat: dpb.New(lhb),
Health: health,
+ TpmUsage: node.tpmUsage,
}
// Evaluate the filter expression for this node. Send the node, if it's
diff --git a/metropolis/node/core/curator/impl_leader_test.go b/metropolis/node/core/curator/impl_leader_test.go
index b40ad54..c28ba7a 100644
--- a/metropolis/node/core/curator/impl_leader_test.go
+++ b/metropolis/node/core/curator/impl_leader_test.go
@@ -8,6 +8,7 @@
"crypto/tls"
"crypto/x509"
"encoding/hex"
+ "fmt"
"io"
"net"
"strings"
@@ -44,7 +45,7 @@
//
// This is used to test functionality of the individual curatorLeader RPC
// implementations without the overhead of having to wait for a leader election.
-func fakeLeader(t *testing.T) fakeLeaderData {
+func fakeLeader(t *testing.T, opts ...*fakeLeaderOption) fakeLeaderData {
t.Helper()
lt := logtree.New()
logtree.PipeAllToTest(t, lt)
@@ -52,6 +53,14 @@
// terminating all harnesses started by this function.
ctx, ctxC := context.WithCancel(context.Background())
+ // Merge all options into a single struct.
+ var opt fakeLeaderOption
+ for _, optI := range opts {
+ if optI.icc != nil {
+ opt.icc = optI.icc
+ }
+ }
+
// Start a single-node etcd cluster.
integration.BeforeTestExternal(t)
grpclog.SetLoggerV2(logtree.GRPCify(lt.MustLeveledFor("grpc")))
@@ -94,12 +103,19 @@
if err != nil {
t.Fatalf("could not generate node keypair: %v", err)
}
- cNode := NewNodeForBootstrap(nil, nodePub, nodeJoinPub)
+ cNode := NewNodeForBootstrap(nil, nodePub, nodeJoinPub, cpb.NodeTPMUsage_NODE_TPM_PRESENT_AND_USED)
// Here we would enable the leader node's roles. But for tests, we don't enable
// any.
- caCertBytes, nodeCertBytes, err := BootstrapNodeFinish(ctx, curEtcd, &cNode, nil, DefaultClusterConfiguration())
+ cc := DefaultClusterConfiguration()
+ if opt.icc != nil {
+ cc, err = ClusterConfigurationFromInitial(opt.icc)
+ if err != nil {
+ t.Fatalf("invalid initial cluster options: %v", err)
+ }
+ }
+ caCertBytes, nodeCertBytes, err := BootstrapNodeFinish(ctx, curEtcd, &cNode, nil, cc)
if err != nil {
t.Fatalf("could not finish node bootstrap: %v", err)
}
@@ -227,6 +243,12 @@
}
}
+type fakeLeaderOption struct {
+ // icc is the initial cluster configuration to be set when bootstrapping the
+ //fake cluster. If not set, uses system defaults.
+ icc *cpb.ClusterConfiguration
+}
+
// fakeLeaderData is returned by fakeLeader and contains information about the
// newly created leader and connections to its gRPC listeners.
type fakeLeaderData struct {
@@ -620,12 +642,12 @@
if err != nil {
t.Fatalf("could not generate node join keypair: %v", err)
}
-
// Register 'other node' into cluster.
cur := ipb.NewCuratorClient(cl.otherNodeConn)
_, err = cur.RegisterNode(ctx, &ipb.RegisterNodeRequest{
RegisterTicket: res1.Ticket,
JoinKey: nodeJoinPub,
+ HaveLocalTpm: true,
})
if err != nil {
t.Fatalf("RegisterNode failed: %v", err)
@@ -710,6 +732,7 @@
pubkey: npub,
jkey: jpub,
state: cpb.NodeState_NODE_STATE_UP,
+ tpmUsage: cpb.NodeTPMUsage_NODE_TPM_PRESENT_AND_USED,
}
if err := nodeSave(ctx, cl.l, &node); err != nil {
t.Fatalf("nodeSave failed: %v", err)
@@ -729,7 +752,9 @@
t.Fatalf("Dialing external GRPC failed: %v", err)
}
cur := ipb.NewCuratorClient(eph)
- jr, err := cur.JoinNode(ctx, &ipb.JoinNodeRequest{})
+ jr, err := cur.JoinNode(ctx, &ipb.JoinNodeRequest{
+ UsingSealedConfiguration: true,
+ })
if err != nil {
t.Fatalf("JoinNode failed: %v", err)
}
@@ -1478,3 +1503,114 @@
t.Errorf("Adding same pubkey to different node should have failed, got %v", err)
}
}
+
+// TestClusterTPMModeSetting exercises the TPM mode logic present in the Register
+// and Join methods of the Curator.
+func TestClusterTPMModeSetting(t *testing.T) {
+ ctx, ctxC := context.WithCancel(context.Background())
+ defer ctxC()
+
+ for i, te := range []struct {
+ mode cpb.ClusterConfiguration_TPMMode
+ haveTPM bool
+ success bool
+ }{
+ // REQUIRED mode should only allow in nodes with TPM.
+ {cpb.ClusterConfiguration_TPM_MODE_REQUIRED, true, true},
+ {cpb.ClusterConfiguration_TPM_MODE_REQUIRED, false, false},
+ // BEST_EFFORT mode should allow nodes with and without TPM.
+ {cpb.ClusterConfiguration_TPM_MODE_BEST_EFFORT, true, true},
+ {cpb.ClusterConfiguration_TPM_MODE_BEST_EFFORT, false, true},
+ // DISABLED mode should allow nodes with and without TPM.
+ {cpb.ClusterConfiguration_TPM_MODE_DISABLED, true, true},
+ {cpb.ClusterConfiguration_TPM_MODE_DISABLED, false, true},
+ } {
+ t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+ cl := fakeLeader(t, &fakeLeaderOption{
+ icc: &cpb.ClusterConfiguration{
+ TpmMode: te.mode,
+ },
+ })
+ // Register node and make sure it's either successful or not, depending on the
+ // table test success value.
+ mgmt := apb.NewManagementClient(cl.mgmtConn)
+ resT, err := mgmt.GetRegisterTicket(ctx, &apb.GetRegisterTicketRequest{})
+ if err != nil {
+ t.Fatalf("GetRegisterTicket failed: %v", err)
+ }
+ nodeJoinPub, nodeJoinPriv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatalf("could not generate node join keypair: %v", err)
+ }
+ cur := ipb.NewCuratorClient(cl.otherNodeConn)
+ resR, err := cur.RegisterNode(ctx, &ipb.RegisterNodeRequest{
+ RegisterTicket: resT.Ticket,
+ JoinKey: nodeJoinPub,
+ HaveLocalTpm: te.haveTPM,
+ })
+ if te.success && err != nil {
+ t.Errorf("expected success, got %v", err)
+ }
+ if !te.success && err == nil {
+ t.Errorf("should have failed")
+ }
+
+ // Nothing else to do if the test variant was supposed to fail.
+ if !te.success {
+ return
+ }
+
+ // Finish node registration by approving it and calling commit.
+ otherNodePub := cl.otherNodePriv.Public().(ed25519.PublicKey)
+ _, err = mgmt.ApproveNode(ctx, &apb.ApproveNodeRequest{Pubkey: otherNodePub})
+ if err != nil {
+ t.Fatalf("ApproveNode failed: %v", err)
+ }
+
+ _, err = cur.CommitNode(ctx, &ipb.CommitNodeRequest{
+ ClusterUnlockKey: []byte("fakefakefakefakefakefakefakefake"),
+ })
+ if err != nil {
+ t.Fatalf("CommitNode failed: %v", err)
+ }
+
+ // Node registered as expected. Now make sure it can only join with the same seal
+ // state as it was supposed to use.
+ ephCreds, err := rpc.NewEphemeralCredentials(nodeJoinPriv, cl.ca)
+ if err != nil {
+ t.Fatalf("NewEphemeralCredentials: %v", err)
+ }
+ withLocalDialer := grpc.WithContextDialer(func(_ context.Context, _ string) (net.Conn, error) {
+ return cl.curatorLis.Dial()
+ })
+ eph, err := grpc.Dial("local", withLocalDialer, grpc.WithTransportCredentials(ephCreds))
+ if err != nil {
+ t.Fatalf("Dialing external GRPC failed: %v", err)
+ }
+ t.Cleanup(func() {
+ eph.Close()
+ })
+
+ useTPM := false
+ if resR.TpmUsage == cpb.NodeTPMUsage_NODE_TPM_PRESENT_AND_USED {
+ useTPM = true
+ }
+
+ // First try with the wrong state. That should fail.
+ curJ := ipb.NewCuratorClient(eph)
+ _, err = curJ.JoinNode(ctx, &ipb.JoinNodeRequest{
+ UsingSealedConfiguration: !useTPM,
+ })
+ if err == nil {
+ t.Fatalf("curator should have rejected invalid UsingSealedConfiguration setting")
+ }
+ // Now with the expected state. That should succeed.
+ _, err = curJ.JoinNode(ctx, &ipb.JoinNodeRequest{
+ UsingSealedConfiguration: useTPM,
+ })
+ if err != nil {
+ t.Fatalf("join failed: %v", err)
+ }
+ })
+ }
+}
diff --git a/metropolis/node/core/curator/proto/api/api.proto b/metropolis/node/core/curator/proto/api/api.proto
index e9ead1d..ea264e5 100644
--- a/metropolis/node/core/curator/proto/api/api.proto
+++ b/metropolis/node/core/curator/proto/api/api.proto
@@ -255,9 +255,18 @@
// join_key is an ED25519 public key generated during registration. It's
// shared with Curator to authenticate the join procedure later on.
bytes join_key = 2;
+ // have_local_tpm is set by the node if it has a local TPM2.0 that it
+ // successfully initialized. This information should be verified by the
+ // Curator in high-assurance scenarios using hardware attestation.
+ bool have_local_tpm = 3;
}
message RegisterNodeResponse {
+ // cluster_configuration is currently returned to the node just for
+ // informative reasons.
+ metropolis.proto.common.ClusterConfiguration cluster_configuration = 1;
+ // tpm_usage tells the node whether it should use its TPM or not.
+ metropolis.proto.common.NodeTPMUsage tpm_usage = 2;
}
message CommitNodeRequest {
@@ -280,6 +289,15 @@
}
message JoinNodeRequest {
+ // using_sealed_configuration is set by the node if it has loaded its join
+ // keys and NUK from a sealed secret on the EFI system partition. If it did,
+ // it means it was previously configured to use the TPM 2.0 to seal it.
+ //
+ // Naturally, this information should be verified by the Curator
+ // in high-assurance scenarios using hardware attestation. Ie., if the node
+ // claims to have loaded its keys from the TPM, then it should also be able
+ // to prove that it's running using that TPM.
+ bool using_sealed_configuration = 1;
}
message JoinNodeResponse {
diff --git a/metropolis/node/core/curator/proto/private/storage.proto b/metropolis/node/core/curator/proto/private/storage.proto
index 8344041..d279a28 100644
--- a/metropolis/node/core/curator/proto/private/storage.proto
+++ b/metropolis/node/core/curator/proto/private/storage.proto
@@ -35,6 +35,10 @@
// metropolis.node.core.curator.api.Curator.UpdateNodeClusterNetworking for
// more details.
metropolis.proto.common.NodeClusterNetworking clusternet = 7;
+
+ // tpm_usage describes whether this node has a TPM 2.0 and whether it's using
+ // it.
+ metropolis.proto.common.NodeTPMUsage tpm_usage = 8;
}
// Information about the cluster owner, currently the only Metropolis management
diff --git a/metropolis/node/core/curator/state_cluster.go b/metropolis/node/core/curator/state_cluster.go
index b62fe07..226bf53 100644
--- a/metropolis/node/core/curator/state_cluster.go
+++ b/metropolis/node/core/curator/state_cluster.go
@@ -40,7 +40,12 @@
return clusterFromProto(icc)
}
-func (c *Cluster) UseTPM(available bool) (bool, error) {
+// NodeShouldUseTPM returns whether a node should use a TPM or not for this Cluster
+// and a given node's TPM availability.
+//
+// A user-facing error is returned if the combination of local cluster policy and
+// node TPM availability is invalid.
+func (c *Cluster) NodeShouldUseTPM(available bool) (bool, error) {
switch c.TPMMode {
case cpb.ClusterConfiguration_TPM_MODE_DISABLED:
return false, nil
@@ -56,6 +61,29 @@
}
}
+// NodeTPMUsage returns the NodeTPMUsage (whether a node should use a TPM or not
+// plus information whether it has a TPM in the first place) for this Cluster and
+// a given node's TPM availability.
+//
+// A user-facing error is returned if the combination of local cluster policy and
+// node TPM availability is invalid.
+func (c *Cluster) NodeTPMUsage(have bool) (usage cpb.NodeTPMUsage, err error) {
+ var use bool
+ use, err = c.NodeShouldUseTPM(have)
+ if err != nil {
+ return
+ }
+ switch {
+ case have && use:
+ usage = cpb.NodeTPMUsage_NODE_TPM_PRESENT_AND_USED
+ case have && !use:
+ usage = cpb.NodeTPMUsage_NODE_TPM_PRESENT_BUT_UNUSED
+ case !have:
+ usage = cpb.NodeTPMUsage_NODE_TPM_NOT_PRESENT
+ }
+ return
+}
+
func clusterUnmarshal(data []byte) (*Cluster, error) {
msg := cpb.ClusterConfiguration{}
if err := proto.Unmarshal(data, &msg); err != nil {
diff --git a/metropolis/node/core/curator/state_node.go b/metropolis/node/core/curator/state_node.go
index 2250ce1..1b53dee 100644
--- a/metropolis/node/core/curator/state_node.go
+++ b/metropolis/node/core/curator/state_node.go
@@ -75,6 +75,8 @@
status *cpb.NodeStatus
+ tpmUsage cpb.NodeTPMUsage
+
// A Node can have multiple Roles. Each Role is represented by the presence
// of NodeRole* structures in this structure, with a nil pointer
// representing the lack of a role.
@@ -103,12 +105,13 @@
// cluster state.
//
// This can only be used by the cluster bootstrap logic.
-func NewNodeForBootstrap(cuk, pubkey, jpub []byte) Node {
+func NewNodeForBootstrap(cuk, pubkey, jpub []byte, tpmUsage cpb.NodeTPMUsage) Node {
return Node{
clusterUnlockKey: cuk,
pubkey: pubkey,
jkey: jpub,
state: cpb.NodeState_NODE_STATE_UP,
+ tpmUsage: tpmUsage,
}
}
@@ -243,6 +246,7 @@
FsmState: n.state,
Roles: &cpb.NodeRoles{},
Status: n.status,
+ TpmUsage: n.tpmUsage,
}
if n.kubernetesWorker != nil {
msg.Roles.KubernetesWorker = &cpb.NodeRoles_KubernetesWorker{}
@@ -291,6 +295,7 @@
jkey: msg.JoinKey,
state: msg.FsmState,
status: msg.Status,
+ tpmUsage: msg.TpmUsage,
}
if msg.Roles.KubernetesWorker != nil {
n.kubernetesWorker = &NodeRoleKubernetesWorker{}
diff --git a/metropolis/node/core/localstorage/storage_esp.go b/metropolis/node/core/localstorage/storage_esp.go
index 05b1f1c..9da948a 100644
--- a/metropolis/node/core/localstorage/storage_esp.go
+++ b/metropolis/node/core/localstorage/storage_esp.go
@@ -152,20 +152,27 @@
return nil
}
-func (e *ESPSealedConfiguration) SealSecureBoot(c *ppb.SealedConfiguration) error {
+func (e *ESPSealedConfiguration) SealSecureBoot(c *ppb.SealedConfiguration, tpmUsage cpb.NodeTPMUsage) error {
bytes, err := proto.Marshal(c)
if err != nil {
return fmt.Errorf("while marshaling: %w", err)
}
- // Use Secure Boot PCRs to seal the configuration.
- // See: TCG PC Client Platform Firmware Profile Specification v1.05,
- // table 3.3.4.1
- // See: https://trustedcomputinggroup.org/wp-content/uploads/
- // TCG_PCClient_PFP_r1p05_v22_02dec2020.pdf
- bytes, err = tpm.Seal(bytes, tpm.SecureBootPCRs)
- if err != nil {
- return fmt.Errorf("while using tpm: %w", err)
+ switch tpmUsage {
+ case cpb.NodeTPMUsage_NODE_TPM_PRESENT_AND_USED:
+ // Use Secure Boot PCRs to seal the configuration.
+ // See: TCG PC Client Platform Firmware Profile Specification v1.05,
+ // table 3.3.4.1
+ // See: https://trustedcomputinggroup.org/wp-content/uploads/
+ // TCG_PCClient_PFP_r1p05_v22_02dec2020.pdf
+ bytes, err = tpm.Seal(bytes, tpm.SecureBootPCRs)
+ if err != nil {
+ return fmt.Errorf("while using tpm: %w", err)
+ }
+ case cpb.NodeTPMUsage_NODE_TPM_PRESENT_BUT_UNUSED:
+ case cpb.NodeTPMUsage_NODE_TPM_NOT_PRESENT:
+ default:
+ return fmt.Errorf("unknown tpmUsage %d", tpmUsage)
}
if err := e.Write(bytes, 0644); err != nil {
@@ -196,3 +203,21 @@
return &config, nil
}
+
+func (e *ESPSealedConfiguration) ReadUnsafe() (*ppb.SealedConfiguration, error) {
+ bytes, err := e.Read()
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil, ErrNoSealed
+ }
+ return nil, fmt.Errorf("%w: when reading sealed data: %v", ErrSealedUnavailable, err)
+ }
+
+ config := ppb.SealedConfiguration{}
+ err = proto.Unmarshal(bytes, &config)
+ if err != nil {
+ return nil, fmt.Errorf("%w: when unmarshaling: %v", ErrSealedCorrupted, err)
+ }
+
+ return &config, nil
+}
diff --git a/metropolis/node/core/roleserve/roleserve.go b/metropolis/node/core/roleserve/roleserve.go
index f97e9c9..c1493be 100644
--- a/metropolis/node/core/roleserve/roleserve.go
+++ b/metropolis/node/core/roleserve/roleserve.go
@@ -165,7 +165,7 @@
return s
}
-func (s *Service) ProvideBootstrapData(privkey ed25519.PrivateKey, iok, cuk, nuk, jkey []byte, icc *curator.Cluster) {
+func (s *Service) ProvideBootstrapData(privkey ed25519.PrivateKey, iok, cuk, nuk, jkey []byte, icc *curator.Cluster, tpmUsage cpb.NodeTPMUsage) {
pubkey := privkey.Public().(ed25519.PublicKey)
nid := identity.NodeID(pubkey)
@@ -184,6 +184,7 @@
nodeUnlockKey: nuk,
nodePrivateJoinKey: jkey,
initialClusterConfiguration: icc,
+ nodeTPMUsage: tpmUsage,
})
}
diff --git a/metropolis/node/core/roleserve/value_bootstrapdata.go b/metropolis/node/core/roleserve/value_bootstrapdata.go
index 90af955..f2ed064 100644
--- a/metropolis/node/core/roleserve/value_bootstrapdata.go
+++ b/metropolis/node/core/roleserve/value_bootstrapdata.go
@@ -4,6 +4,7 @@
"crypto/ed25519"
"source.monogon.dev/metropolis/node/core/curator"
+ cpb "source.monogon.dev/metropolis/proto/common"
)
// bootstrapData is an internal EventValue structure which is populated by the
@@ -17,4 +18,5 @@
initialOwnerKey []byte
nodePrivateJoinKey ed25519.PrivateKey
initialClusterConfiguration *curator.Cluster
+ nodeTPMUsage cpb.NodeTPMUsage
}
diff --git a/metropolis/node/core/roleserve/worker_controlplane.go b/metropolis/node/core/roleserve/worker_controlplane.go
index 1a8e420..d21df70 100644
--- a/metropolis/node/core/roleserve/worker_controlplane.go
+++ b/metropolis/node/core/roleserve/worker_controlplane.go
@@ -283,7 +283,7 @@
npub := b.nodePrivateKey.Public().(ed25519.PublicKey)
jpub := b.nodePrivateJoinKey.Public().(ed25519.PublicKey)
- n := curator.NewNodeForBootstrap(b.clusterUnlockKey, npub, jpub)
+ n := curator.NewNodeForBootstrap(b.clusterUnlockKey, npub, jpub, b.nodeTPMUsage)
// The first node always runs consensus.
join, err := st.AddNode(ctx, npub)
@@ -369,7 +369,7 @@
JoinKey: b.nodePrivateJoinKey,
ClusterCa: caCert,
}
- if err = s.storageRoot.ESP.Metropolis.SealedConfiguration.SealSecureBoot(&sc); err != nil {
+ if err = s.storageRoot.ESP.Metropolis.SealedConfiguration.SealSecureBoot(&sc, b.nodeTPMUsage); err != nil {
return fmt.Errorf("writing sealed configuration failed: %w", err)
}