Skip to content

Commit 508d9dd

Browse files
committed
do config update
1 parent 1112b1c commit 508d9dd

5 files changed

Lines changed: 246 additions & 3 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+
// FetchConfigPrivateKey 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: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,117 @@ func (c *Client) DoUpdate(ctx context.Context, creds keys.Credentials) ([]byte,
376376
return result.Config, nebulaPrivkeyPEM, newCreds, meta, nil
377377
}
378378

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

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)