@@ -2,12 +2,18 @@ package main
2
2
3
3
import (
4
4
"context"
5
+ "crypto/ecdsa"
6
+ "crypto/sha256"
7
+ "crypto/x509"
5
8
"encoding/hex"
6
9
"encoding/json"
10
+ "encoding/pem"
7
11
"fmt"
8
12
"io"
13
+ "log/slog"
9
14
"net"
10
15
"os"
16
+ "slices"
11
17
"time"
12
18
13
19
"github.com/edgelesssys/nunki/internal/atls"
@@ -45,6 +51,7 @@ func newSetCmd() *cobra.Command {
45
51
cmd .Flags ().StringP ("coordinator" , "c" , "" , "endpoint the coordinator can be reached at" )
46
52
must (cobra .MarkFlagRequired (cmd .Flags (), "coordinator" ))
47
53
cmd .Flags ().String ("coordinator-policy-hash" , DefaultCoordinatorPolicyHash , "expected policy hash of the coordinator, will not be checked if empty" )
54
+ cmd .Flags ().String ("workload-owner-key" , workloadOwnerPEM , "path to workload owner key (.pem) file" )
48
55
49
56
return cmd
50
57
}
@@ -69,6 +76,11 @@ func runSet(cmd *cobra.Command, args []string) error {
69
76
return fmt .Errorf ("failed to unmarshal manifest: %w" , err )
70
77
}
71
78
79
+ workloadOwnerKey , err := loadWorkloadOwnerKey (flags .workloadOwnerKeyPath , m , log )
80
+ if err != nil {
81
+ return fmt .Errorf ("loading workload owner key: %w" , err )
82
+ }
83
+
72
84
paths , err := findGenerateTargets (args , log )
73
85
if err != nil {
74
86
return fmt .Errorf ("finding yaml files: %w" , err )
@@ -92,7 +104,7 @@ func runSet(cmd *cobra.Command, args []string) error {
92
104
kdsCache := fsstore .New (kdsDir , log .WithGroup ("kds-cache" ))
93
105
kdsGetter := snp .NewCachedHTTPSGetter (kdsCache , snp .NeverGCTicker , log .WithGroup ("kds-getter" ))
94
106
validator := snp .NewValidator (validateOptsGen , kdsGetter , log .WithGroup ("snp-validator" ))
95
- dialer := dialer .New (atls .NoIssuer , validator , & net.Dialer {})
107
+ dialer := dialer .NewWithKey (atls .NoIssuer , validator , & net.Dialer {}, workloadOwnerKey )
96
108
97
109
conn , err := dialer .Dial (cmd .Context (), flags .coordinator )
98
110
if err != nil {
@@ -107,6 +119,18 @@ func runSet(cmd *cobra.Command, args []string) error {
107
119
}
108
120
resp , err := setLoop (cmd .Context (), client , cmd .OutOrStdout (), req )
109
121
if err != nil {
122
+ grpcSt , ok := status .FromError (err )
123
+ if ok {
124
+ if grpcSt .Code () == codes .PermissionDenied {
125
+ msg := "Permission denied."
126
+ if workloadOwnerKey == nil {
127
+ msg += " Specify a workload owner key with --workload-owner-key."
128
+ } else {
129
+ msg += " Ensure you are using a trusted workload owner key."
130
+ }
131
+ fmt .Fprintln (cmd .OutOrStdout (), msg )
132
+ }
133
+ }
110
134
return fmt .Errorf ("failed to set manifest: %w" , err )
111
135
}
112
136
@@ -124,9 +148,10 @@ func runSet(cmd *cobra.Command, args []string) error {
124
148
}
125
149
126
150
type setFlags struct {
127
- manifestPath string
128
- coordinator string
129
- policy []byte
151
+ manifestPath string
152
+ coordinator string
153
+ policy []byte
154
+ workloadOwnerKeyPath string
130
155
}
131
156
132
157
func parseSetFlags (cmd * cobra.Command ) (* setFlags , error ) {
@@ -149,6 +174,10 @@ func parseSetFlags(cmd *cobra.Command) (*setFlags, error) {
149
174
if err != nil {
150
175
return nil , fmt .Errorf ("hex-decoding coordinator-policy-hash flag: %w" , err )
151
176
}
177
+ flags .workloadOwnerKeyPath , err = cmd .Flags ().GetString ("workload-owner-key" )
178
+ if err != nil {
179
+ return nil , fmt .Errorf ("getting workload-owner-key flag: %w" , err )
180
+ }
152
181
153
182
return flags , nil
154
183
}
@@ -161,6 +190,42 @@ func policyMapToBytesList(m map[string]deployment) [][]byte {
161
190
return policies
162
191
}
163
192
193
+ func loadWorkloadOwnerKey (path string , manifst manifest.Manifest , log * slog.Logger ) (* ecdsa.PrivateKey , error ) {
194
+ key , err := os .ReadFile (path )
195
+ if os .IsNotExist (err ) {
196
+ return nil , nil
197
+ }
198
+ if err != nil {
199
+ return nil , fmt .Errorf ("reading workload owner key: %w" , err )
200
+ }
201
+ pemBlock , _ := pem .Decode (key )
202
+ if pemBlock == nil {
203
+ return nil , fmt .Errorf ("decoding workload owner key: %w" , err )
204
+ }
205
+ if pemBlock .Type != "EC PRIVATE KEY" {
206
+ return nil , fmt .Errorf ("workload owner key is not an EC private key" )
207
+ }
208
+ workloadOwnerKey , err := x509 .ParseECPrivateKey (pemBlock .Bytes )
209
+ if err != nil {
210
+ return nil , fmt .Errorf ("parsing workload owner key: %w" , err )
211
+ }
212
+ pubKey , err := x509 .MarshalPKIXPublicKey (& workloadOwnerKey .PublicKey )
213
+ if err != nil {
214
+ return nil , fmt .Errorf ("marshaling public key: %w" , err )
215
+ }
216
+ ownerKeyHash := sha256 .Sum256 (pubKey )
217
+ ownerKeyHex := manifest .NewHexString (ownerKeyHash [:])
218
+ if len (manifst .WorkloadOwnerKeyDigests ) == 0 {
219
+ log .Warn ("No workload owner keys in manifest. Further manifest updates will be rejected by the coordinator" )
220
+ return workloadOwnerKey , nil
221
+ }
222
+ log .Debug ("Workload owner keys in manifest" , "keys" , manifst .WorkloadOwnerKeyDigests )
223
+ if ! slices .Contains (manifst .WorkloadOwnerKeyDigests , ownerKeyHex ) {
224
+ log .Warn ("Workload owner key not found in manifest. This may lock you out from further updates" )
225
+ }
226
+ return workloadOwnerKey , nil
227
+ }
228
+
164
229
func setLoop (
165
230
ctx context.Context , client coordapi.CoordAPIClient , out io.Writer , req * coordapi.SetManifestRequest ,
166
231
) (resp * coordapi.SetManifestResponse , retErr error ) {
0 commit comments