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