Skip to content

Commit 11416b3

Browse files
authored
do config update (#36)
* do config update supports an update flow where only the configuration is updated; cert and key remain unchanged.
1 parent 8ae1e67 commit 11416b3

5 files changed

Lines changed: 264 additions & 32 deletions

File tree

apiutil.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,67 @@ func InsertConfigPrivateKey(config []byte, privkey []byte) ([]byte, error) {
2828

2929
return yaml.Marshal(y)
3030
}
31+
32+
// InsertConfigCert takes a Nebula YAML and a Nebula PEM-formatted host certifiate, and inserts the certificate into
33+
// the config, overwriting any previous value stored.
34+
func InsertConfigCert(config []byte, cert []byte) ([]byte, error) {
35+
var y map[any]any
36+
if err := yaml.Unmarshal(config, &y); err != nil {
37+
return nil, fmt.Errorf("failed to unmarshal config: %s", err)
38+
}
39+
40+
_, ok := y["pki"]
41+
if !ok {
42+
return nil, fmt.Errorf("config is missing expected pki section")
43+
}
44+
45+
_, ok = y["pki"].(map[any]any)
46+
if !ok {
47+
return nil, fmt.Errorf("config has unexpected value for pki section")
48+
}
49+
50+
y["pki"].(map[any]any)["cert"] = string(cert)
51+
52+
return yaml.Marshal(y)
53+
}
54+
55+
// FetchConfigPrivateKeyAndCert takes a Nebula YAML, finds and returns its contained Nebula PEM-formatted private key,
56+
// the Nebula PEM-formatted host cert, or an error.
57+
func FetchConfigPrivateKeyAndCert(config []byte) ([]byte, []byte, error) {
58+
var y map[any]any
59+
if err := yaml.Unmarshal(config, &y); err != nil {
60+
return nil, nil, fmt.Errorf("failed to unmarshal config: %s", err)
61+
}
62+
63+
_, ok := y["pki"]
64+
if !ok {
65+
return nil, nil, fmt.Errorf("config is missing expected pki section")
66+
}
67+
68+
pki, ok := y["pki"].(map[any]any)
69+
if !ok {
70+
return nil, nil, fmt.Errorf("config has unexpected value for pki section")
71+
}
72+
73+
configKey, ok := pki["key"]
74+
if !ok {
75+
return nil, nil, fmt.Errorf("(%s) config is missing section 'key'", config)
76+
}
77+
78+
existingKey, ok := configKey.(string)
79+
if !ok {
80+
return nil, nil, fmt.Errorf("config section 'key' found but has unexpected type: %T", configKey)
81+
}
82+
83+
configCert, ok := pki["cert"]
84+
if !ok {
85+
return nil, nil, fmt.Errorf("config is missing 'cert' section")
86+
}
87+
88+
existingCert, ok := configCert.(string)
89+
if !ok {
90+
return nil, nil, fmt.Errorf("config section 'cert' found but has unexpected type: %T", configCert)
91+
}
92+
93+
return []byte(existingKey), []byte(existingCert), nil
94+
}

apiutil_test.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package dnapi
22

33
import (
4+
"fmt"
45
"testing"
56

7+
"github.com/stretchr/testify/assert"
68
"github.com/stretchr/testify/require"
79
"gopkg.in/yaml.v2"
810
)
@@ -13,13 +15,32 @@ pki: {}
1315
`), []byte("foobar"))
1416
require.NoError(t, err)
1517

16-
var y map[string]interface{}
18+
var y map[string]any
1719
err = yaml.Unmarshal(cfg, &y)
1820
require.NoError(t, err)
1921

20-
require.Equal(t, "foobar", y["pki"].(map[interface{}]interface{})["key"])
22+
require.Equal(t, "foobar", y["pki"].(map[any]any)["key"])
2123

22-
cfg, err = InsertConfigPrivateKey([]byte(``), []byte("foobar"))
24+
_, err = InsertConfigPrivateKey([]byte(``), []byte("foobar"))
2325
require.Error(t, err)
2426

2527
}
28+
29+
func TestFetchConfigPrivateKey(t *testing.T) {
30+
keyValue := []byte("foobar")
31+
certValue := []byte("lolwat")
32+
33+
configValue := fmt.Sprintf(`pki: { cert: %s }`, certValue)
34+
cfg, err := InsertConfigPrivateKey([]byte(configValue), keyValue)
35+
require.NoError(t, err)
36+
37+
var y map[string]any
38+
err = yaml.Unmarshal(cfg, &y)
39+
require.NoError(t, err)
40+
require.Equal(t, keyValue, []byte(y["pki"].(map[any]any)["key"].(string)))
41+
42+
fetchedVal, fetchedCert, err := FetchConfigPrivateKeyAndCert(cfg)
43+
require.NoError(t, err)
44+
assert.Equal(t, certValue, fetchedCert)
45+
assert.Equal(t, keyValue, fetchedVal)
46+
}

client.go

Lines changed: 129 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -306,22 +306,11 @@ func (c *Client) DoUpdate(ctx context.Context, creds keys.Credentials) ([]byte,
306306
if err != nil {
307307
return nil, nil, nil, nil, fmt.Errorf("failed to make API call to Defined Networking: %w", err)
308308
}
309-
resultWrapper := message.SignedResponseWrapper{}
310-
err = json.Unmarshal(resp, &resultWrapper)
311-
if err != nil {
312-
return nil, nil, nil, nil, fmt.Errorf("failed to unmarshal signed response wrapper: %s", err)
313-
}
314309

315310
// Verify the signature
316-
valid := false
317-
for _, caPubkey := range creds.TrustedKeys {
318-
if caPubkey.Verify(resultWrapper.Data.Message, resultWrapper.Data.Signature) {
319-
valid = true
320-
break
321-
}
322-
}
323-
if !valid {
324-
return nil, nil, nil, nil, fmt.Errorf("failed to verify signed API result")
311+
resultWrapper, err := verifySignature(resp, creds)
312+
if err != nil {
313+
return nil, nil, nil, nil, err
325314
}
326315

327316
// Consume the verified message
@@ -379,6 +368,130 @@ func (c *Client) DoUpdate(ctx context.Context, creds keys.Credentials) ([]byte,
379368
return result.Config, nebulaPrivkeyPEM, newCreds, meta, nil
380369
}
381370

371+
// DoConfigUpdate sends a signed message to the DNClient API to fetch the new configuration update. During this call new keys
372+
// are generated for DNClient API communication. If the API response is successful, the new configuration
373+
// is returned along with the new DNClient API credentials and a meta object.
374+
//
375+
// See dnapi.InsertConfigPrivateKey and dnapi.InsertConfigCert for how to insert the old Nebula cert/private key into the configuration.
376+
func (c *Client) DoConfigUpdate(ctx context.Context, creds keys.Credentials) ([]byte, *keys.Credentials, *ConfigMeta, error) {
377+
// Rotate key
378+
var hostPrivkey keys.PrivateKey // ECDSA
379+
380+
newKeys, err := keys.New()
381+
if err != nil {
382+
return nil, nil, nil, fmt.Errorf("failed to generate new keys: %s", err)
383+
}
384+
385+
msg := message.DoConfigUpdateRequest{
386+
Nonce: nonce(),
387+
}
388+
389+
// Set the correct keypair based on the current private key type
390+
switch creds.PrivateKey.Unwrap().(type) {
391+
case ed25519.PrivateKey:
392+
hostPubkeyPEM, err := newKeys.HostEd25519PublicKey.MarshalPEM()
393+
if err != nil {
394+
return nil, nil, nil, fmt.Errorf("failed to marshal Ed25519 public key: %s", err)
395+
}
396+
hostPrivkey = newKeys.HostEd25519PrivateKey
397+
msg.HostPubkeyEd25519 = hostPubkeyPEM
398+
case *ecdsa.PrivateKey:
399+
hostPubkeyPEM, err := newKeys.HostP256PublicKey.MarshalPEM()
400+
if err != nil {
401+
return nil, nil, nil, fmt.Errorf("failed to marshal P256 public key: %s", err)
402+
}
403+
hostPrivkey = newKeys.HostP256PrivateKey
404+
msg.HostPubkeyP256 = hostPubkeyPEM
405+
}
406+
407+
blob, err := json.Marshal(msg)
408+
if err != nil {
409+
return nil, nil, nil, fmt.Errorf("failed to marshal DNClient message: %s", err)
410+
}
411+
412+
// Make API call
413+
resp, err := c.postDNClient(ctx, message.DoConfigUpdate, blob, creds.HostID, creds.Counter, creds.PrivateKey)
414+
if err != nil {
415+
return nil, nil, nil, fmt.Errorf("failed to make API call to Defined Networking: %w", err)
416+
}
417+
418+
// Verify the signature
419+
resultWrapper, err := verifySignature(resp, creds)
420+
if err != nil {
421+
return nil, nil, nil, err
422+
}
423+
424+
// Consume the verified message
425+
result := message.DoConfigUpdateResponse{}
426+
err = json.Unmarshal(resultWrapper.Data.Message, &result)
427+
if err != nil {
428+
return nil, nil, nil, fmt.Errorf("failed to unmarshal response (%s): %s", resultWrapper.Data.Message, err)
429+
}
430+
431+
// Verify the nonce
432+
if !bytes.Equal(result.Nonce, msg.Nonce) {
433+
return nil, nil, nil, fmt.Errorf("nonce mismatch between request (%s) and response (%s)", msg.Nonce, result.Nonce)
434+
}
435+
436+
// Verify the counter
437+
if result.Counter <= creds.Counter {
438+
return nil, nil, nil, fmt.Errorf("counter in request (%d) should be less than counter in response (%d)", creds.Counter, result.Counter)
439+
}
440+
441+
trustedKeys, err := keys.TrustedKeysFromPEM(result.TrustedKeys)
442+
if err != nil {
443+
return nil, nil, nil, fmt.Errorf("failed to load trusted keys from bundle: %s", err)
444+
}
445+
446+
newCreds := &keys.Credentials{
447+
HostID: creds.HostID,
448+
Counter: result.Counter,
449+
PrivateKey: hostPrivkey,
450+
TrustedKeys: trustedKeys,
451+
}
452+
453+
meta := &ConfigMeta{
454+
Org: ConfigOrg{
455+
ID: result.Organization.ID,
456+
Name: result.Organization.Name,
457+
},
458+
Network: ConfigNetwork{
459+
ID: result.Network.ID,
460+
Name: result.Network.Name,
461+
},
462+
Host: ConfigHost{
463+
ID: result.Host.ID,
464+
Name: result.Host.Name,
465+
IPAddress: result.Host.IPAddress,
466+
},
467+
}
468+
469+
return result.Config, newCreds, meta, nil
470+
}
471+
472+
// verifySignature is a helper function that takes in an API call repsonse message and
473+
// ensures it is signed by a trusted key. It returns the JSON unmarshalled response section
474+
// if the message is valid JSON and the signature is trusted, otherwise it returns an error.
475+
func verifySignature(resp []byte, creds keys.Credentials) (message.SignedResponseWrapper, error) {
476+
resultWrapper := message.SignedResponseWrapper{}
477+
err := json.Unmarshal(resp, &resultWrapper)
478+
if err != nil {
479+
return message.SignedResponseWrapper{}, fmt.Errorf("failed to unmarshal signed response wrapper: %s", err)
480+
}
481+
482+
valid := false
483+
for _, caPubkey := range creds.TrustedKeys {
484+
if caPubkey.Verify(resultWrapper.Data.Message, resultWrapper.Data.Signature) {
485+
valid = true
486+
break
487+
}
488+
}
489+
if !valid {
490+
return message.SignedResponseWrapper{}, fmt.Errorf("failed to verify signed API result")
491+
}
492+
return resultWrapper, nil
493+
}
494+
382495
func (c *Client) CommandResponse(ctx context.Context, creds keys.Credentials, responseToken string, response any) error {
383496
value, err := json.Marshal(message.CommandResponseRequest{
384497
ResponseToken: responseToken,
@@ -414,22 +527,9 @@ func (c *Client) Reauthenticate(ctx context.Context, creds keys.Credentials) (*m
414527
return nil, err
415528
}
416529

417-
resultWrapper := message.SignedResponseWrapper{}
418-
err = json.Unmarshal(resp, &resultWrapper)
530+
resultWrapper, err := verifySignature(resp, creds)
419531
if err != nil {
420-
return nil, fmt.Errorf("failed to unmarshal signed response wrapper: %s", err)
421-
}
422-
423-
// Verify the signature
424-
valid := false
425-
for _, caPubkey := range creds.TrustedKeys {
426-
if caPubkey.Verify(resultWrapper.Data.Message, resultWrapper.Data.Signature) {
427-
valid = true
428-
break
429-
}
430-
}
431-
if !valid {
432-
return nil, fmt.Errorf("failed to verify signed API result")
532+
return nil, err
433533
}
434534

435535
var response message.ReauthenticateResponse

dnapitest/dnapitest.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,34 @@ func (s *Server) handlerDNClient(w http.ResponseWriter, r *http.Request) {
275275
return
276276
}
277277

278+
case message.DoConfigUpdate:
279+
var updateKeys message.DoConfigUpdateRequest
280+
err = json.Unmarshal(msg.Value, &updateKeys)
281+
if err != nil {
282+
s.errors = append(s.errors, fmt.Errorf("failed to unmarshal DoUpdateRequest: %w", err))
283+
http.Error(w, "failed to unmarshal DoUpdateRequest", http.StatusInternalServerError)
284+
return
285+
}
286+
287+
switch s.curve {
288+
case message.NetworkCurve25519:
289+
if err := s.SetEdPubkey(updateKeys.HostPubkeyEd25519); err != nil {
290+
s.errors = append(s.errors, err)
291+
http.Error(w, err.Error(), http.StatusInternalServerError)
292+
return
293+
}
294+
case message.NetworkCurveP256:
295+
if err := s.SetP256Pubkey(updateKeys.HostPubkeyP256); err != nil {
296+
s.errors = append(s.errors, err)
297+
http.Error(w, err.Error(), http.StatusInternalServerError)
298+
return
299+
}
300+
default:
301+
s.errors = append(s.errors, fmt.Errorf("invalid curve"))
302+
http.Error(w, "invalid curve", http.StatusInternalServerError)
303+
return
304+
}
305+
278306
case message.LongPollWait:
279307
var longPoll message.LongPollWaitRequest
280308
err = json.Unmarshal(msg.Value, &longPoll)

message/message.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
const (
1212
CheckForUpdate = "CheckForUpdate"
1313
DoUpdate = "DoUpdate"
14+
DoConfigUpdate = "DoConfigUpdate"
1415
LongPollWait = "LongPollWait"
1516
CommandResponse = "CommandResponse"
1617
Reauthenticate = "Reauthenticate"
@@ -83,6 +84,24 @@ type DoUpdateResponse struct {
8384
EndpointOIDCMeta *HostEndpointOIDCMetadata `json:"endpointOIDC"`
8485
}
8586

87+
// DoConfigUpdateRequest is the request sent for a DoConfigUpdate request.
88+
type DoConfigUpdateRequest struct {
89+
HostPubkeyEd25519 []byte `json:"edPubkeyPEM"` // X25519 (used for signing)
90+
HostPubkeyP256 []byte `json:"p256HostPubkeyPEM"` // P256 (used for signing)
91+
Nonce []byte `json:"nonce"`
92+
}
93+
94+
// DoConfigUpdateResponse is the response generated for a DoConfigUpdate request.
95+
type DoConfigUpdateResponse struct {
96+
Config []byte `json:"config"`
97+
Counter uint `json:"counter"`
98+
Nonce []byte `json:"nonce"`
99+
TrustedKeys []byte `json:"trustedKeys"`
100+
Organization HostOrgMetadata `json:"organization"`
101+
Network HostNetworkMetadata `json:"network"`
102+
Host HostHostMetadata `json:"host"`
103+
}
104+
86105
// LongPollWaitResponseWrapper contains a response to LongPollWait inside "data."
87106
type LongPollWaitResponseWrapper struct {
88107
Data LongPollWaitResponse `json:"data"`

0 commit comments

Comments
 (0)