blob: f193d5c6d61eb9f852bdb06ef88199e4da0a5a61 [file] [log] [blame]
Lorenz Brunaa6b7342019-12-12 02:55:02 +01001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package api
18
19import (
20 "bytes"
21 "context"
22 "crypto/ed25519"
23 "crypto/rand"
24 "crypto/sha256"
25 "crypto/subtle"
26 "crypto/x509"
27 "encoding/base64"
28 "errors"
29 "fmt"
30 "io"
31
Lorenz Brunaa6b7342019-12-12 02:55:02 +010032 "github.com/gogo/protobuf/proto"
33 "go.etcd.io/etcd/clientv3"
34 "go.uber.org/zap"
35 "google.golang.org/grpc/codes"
36 "google.golang.org/grpc/status"
Hendrik Hofstadt8efe51e2020-02-28 12:53:41 +010037
38 "git.monogon.dev/source/nexantic.git/core/generated/api"
39 "git.monogon.dev/source/nexantic.git/core/pkg/tpm"
Lorenz Brunaa6b7342019-12-12 02:55:02 +010040)
41
42const nodesPrefix = "nodes/"
43const enrolmentsPrefix = "enrolments/"
44
45func nodeId(idCert []byte) (string, error) {
46 // Currently we only identify nodes by ID key
47 cert, err := x509.ParseCertificate(idCert)
48 if err != nil {
49 return "", err
50 }
51 pubKey, ok := cert.PublicKey.(ed25519.PublicKey)
52 if !ok {
53 return "", errors.New("invalid node identity certificate")
54 }
55
56 return "smalltown-" + base64.RawStdEncoding.EncodeToString([]byte(pubKey)), nil
57}
58
59func (s *Server) registerNewNode(node *api.Node) error {
60 nodeRaw, err := proto.Marshal(node)
61 if err != nil {
62 return err
63 }
64
65 nodeID, err := nodeId(node.IdCert)
66 if err != nil {
67 return err
68 }
69
70 key := nodesPrefix + nodeID
71
72 // Overwriting nodes is a BadIdea(TM), so make this a Compare-and-Swap
73 res, err := s.getStore().Txn(context.Background()).If(
74 clientv3.Compare(clientv3.CreateRevision(key), "=", 0),
75 ).Then(
76 clientv3.OpPut(key, string(nodeRaw)),
77 ).Commit()
78 if err != nil {
79 return fmt.Errorf("failed to store new node: %w", err)
80 }
81 if !res.Succeeded {
82 s.Logger.Warn("double-registration of node attempted", zap.String("node", nodeID))
83 }
84 return nil
85}
86
87func (s *Server) TPM2BootstrapNode(newNodeInfo *api.NewNodeInfo) (*api.Node, error) {
88 akPublic, err := tpm.GetAKPublic()
89 if err != nil {
90 return nil, err
91 }
92 ekPubkey, ekCert, err := tpm.GetEKPublic()
93 if err != nil {
94 return nil, err
95 }
96 return &api.Node{
97 Address: newNodeInfo.Ip,
98 Integrity: &api.Node_Tpm2{Tpm2: &api.NodeTPM2{
99 AkPub: akPublic,
100 EkCert: ekCert,
101 EkPubkey: ekPubkey,
102 }},
103 GlobalUnlockKey: newNodeInfo.GlobalUnlockKey,
104 IdCert: newNodeInfo.IdCert,
105 State: api.Node_MASTER,
106 }, nil
107}
108
109func (s *Server) TPM2Unlock(unlockServer api.NodeManagementService_TPM2UnlockServer) error {
110 nonce := make([]byte, 32)
111 if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
Leopold Schabel8fba0f82020-01-22 18:46:25 +0100112 return status.Error(codes.Unavailable, "failed to get randomness")
Lorenz Brunaa6b7342019-12-12 02:55:02 +0100113 }
114 if err := unlockServer.Send(&api.TPM2UnlockFlowResponse{
115 Stage: &api.TPM2UnlockFlowResponse_UnlockInit{
116 UnlockInit: &api.TPM2UnlockInit{
117 Nonce: nonce,
118 },
119 },
120 }); err != nil {
121 return err
122 }
123 unlockReqContainer, err := unlockServer.Recv()
124 if err != nil {
125 return err
126 }
127 unlockReqVariant, ok := unlockReqContainer.Stage.(*api.TPM2UnlockFlowRequeset_UnlockRequest)
128 if !ok {
129 return status.Errorf(codes.InvalidArgument, "protocol violation")
130 }
131 unlockRequest := unlockReqVariant.UnlockRequest
132
133 store := s.getStore()
134 // This is safe, etcd does not do relative paths
135 path := nodesPrefix + unlockRequest.NodeId
136 nodeRes, err := store.Get(unlockServer.Context(), path)
137 if err != nil {
138 return status.Error(codes.Unavailable, "consensus request failed")
139 }
140 if nodeRes.Count == 0 {
141 return status.Error(codes.NotFound, "this node does not exist")
142 } else if nodeRes.Count > 1 {
143 panic("invariant violation: more than one node with the same id")
144 }
145 nodeRaw := nodeRes.Kvs[0].Value
146 var node api.Node
147 if err := proto.Unmarshal(nodeRaw, &node); err != nil {
148 s.Logger.Error("Failed to decode node", zap.Error(err))
149 return status.Error(codes.Internal, "invalid node")
150 }
151
152 nodeTPM2, ok := node.Integrity.(*api.Node_Tpm2)
153 if !ok {
154 return status.Error(codes.InvalidArgument, "node not integrity-protected with TPM2")
155 }
156
157 validQuote, err := tpm.VerifyAttestPlatform(nonce, nodeTPM2.Tpm2.AkPub, unlockRequest.Quote, unlockRequest.QuoteSignature)
158 if err != nil {
159 return status.Error(codes.PermissionDenied, "invalid quote")
160 }
161
162 pcrHash := sha256.New()
163 for _, pcr := range unlockRequest.Pcrs {
164 pcrHash.Write(pcr)
165 }
166 expectedPCRHash := pcrHash.Sum(nil)
167
168 if !bytes.Equal(validQuote.AttestedQuoteInfo.PCRDigest, expectedPCRHash) {
169 return status.Error(codes.InvalidArgument, "the quote's PCR hash does not match the supplied PCRs")
170 }
171
172 // TODO: Plug in policy engine to decide if the unlock should actually happen
173
174 return unlockServer.Send(&api.TPM2UnlockFlowResponse{Stage: &api.TPM2UnlockFlowResponse_UnlockResponse{
175 UnlockResponse: &api.TPM2UnlockResponse{
176 GlobalUnlockKey: node.GlobalUnlockKey,
177 },
178 }})
179}
180
181func (s *Server) NewTPM2NodeRegister(registerServer api.NodeManagementService_NewTPM2NodeRegisterServer) error {
182 registerReqContainer, err := registerServer.Recv()
183 if err != nil {
184 return err
185 }
186 registerReqVariant, ok := registerReqContainer.Stage.(*api.TPM2FlowRequest_Register)
187 if !ok {
188 return status.Error(codes.InvalidArgument, "protocol violation")
189 }
190 registerReq := registerReqVariant.Register
191
192 challengeNonce := make([]byte, 32)
193 if _, err := io.ReadFull(rand.Reader, challengeNonce); err != nil {
Leopold Schabel8fba0f82020-01-22 18:46:25 +0100194 return status.Error(codes.Unavailable, "failed to get randomness")
Lorenz Brunaa6b7342019-12-12 02:55:02 +0100195 }
196 challenge, challengeBlob, err := tpm.MakeAKChallenge(registerReq.EkPubkey, registerReq.AkPublic, challengeNonce)
197 if err != nil {
198 return status.Errorf(codes.InvalidArgument, "failed to challenge AK: %v", err)
199 }
200 nonce := make([]byte, 32)
201 if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
Leopold Schabel8fba0f82020-01-22 18:46:25 +0100202 return status.Error(codes.Unavailable, "failed to get randomness")
Lorenz Brunaa6b7342019-12-12 02:55:02 +0100203 }
204 if err := registerServer.Send(&api.TPM2FlowResponse{Stage: &api.TPM2FlowResponse_AttestRequest{AttestRequest: &api.TPM2AttestRequest{
205 AkChallenge: challenge,
206 AkChallengeSecret: challengeBlob,
207 QuoteNonce: nonce,
208 }}}); err != nil {
209 return err
210 }
211 attestationResContainer, err := registerServer.Recv()
212 if err != nil {
213 return err
214 }
215 attestResVariant, ok := attestationResContainer.Stage.(*api.TPM2FlowRequest_AttestResponse)
216 if !ok {
217 return status.Error(codes.InvalidArgument, "protocol violation")
218 }
219 attestRes := attestResVariant.AttestResponse
220
221 if subtle.ConstantTimeCompare(attestRes.AkChallengeSolution, challengeNonce) != 1 {
222 return status.Error(codes.InvalidArgument, "invalid challenge response")
223 }
224
225 validQuote, err := tpm.VerifyAttestPlatform(nonce, registerReq.AkPublic, attestRes.Quote, attestRes.QuoteSignature)
226 if err != nil {
227 return status.Error(codes.PermissionDenied, "invalid quote")
228 }
229
230 pcrHash := sha256.New()
231 for _, pcr := range attestRes.Pcrs {
232 pcrHash.Write(pcr)
233 }
234 expectedPCRHash := pcrHash.Sum(nil)
235
236 if !bytes.Equal(validQuote.AttestedQuoteInfo.PCRDigest, expectedPCRHash) {
237 return status.Error(codes.InvalidArgument, "the quote's PCR hash does not match the supplied PCRs")
238 }
239
240 newNodeInfoContainer, err := registerServer.Recv()
241 newNodeInfoVariant, ok := newNodeInfoContainer.Stage.(*api.TPM2FlowRequest_NewNodeInfo)
242 newNodeInfo := newNodeInfoVariant.NewNodeInfo
243
244 store := s.getStore()
245 res, err := store.Get(registerServer.Context(), "enrolments/"+base64.RawURLEncoding.EncodeToString(newNodeInfo.EnrolmentConfig.EnrolmentSecret))
246 if err != nil {
247 return status.Error(codes.Unavailable, "Consensus unavailable")
248 }
249 if res.Count == 0 {
250 return status.Error(codes.PermissionDenied, "Invalid enrolment secret")
251 } else if res.Count > 1 {
252 panic("more than one value for the same key, bailing")
253 }
254 rawVal := res.Kvs[0].Value
255 var config api.EnrolmentConfig
256 if err := proto.Unmarshal(rawVal, &config); err != nil {
257 return err
258 }
259
260 // TODO: Plug in policy engine here
261
262 node := api.Node{
263 Address: newNodeInfo.Ip,
264 Integrity: &api.Node_Tpm2{Tpm2: &api.NodeTPM2{
265 AkPub: registerReq.AkPublic,
266 EkCert: registerReq.EkCert,
267 EkPubkey: registerReq.EkPubkey,
268 }},
269 GlobalUnlockKey: newNodeInfo.GlobalUnlockKey,
270 IdCert: newNodeInfo.IdCert,
271 State: api.Node_UNININITALIZED,
272 }
273
274 if err := s.registerNewNode(&node); err != nil {
275 s.Logger.Error("failed to register a node", zap.Error(err))
276 return status.Error(codes.Internal, "failed to register node")
277 }
278
279 return nil
280}