Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions apiutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,67 @@ func InsertConfigPrivateKey(config []byte, privkey []byte) ([]byte, error) {

return yaml.Marshal(y)
}

// InsertConfigCert takes a Nebula YAML and a Nebula PEM-formatted host certifiate, and inserts the certificate into
// the config, overwriting any previous value stored.
func InsertConfigCert(config []byte, cert []byte) ([]byte, error) {
var y map[any]any
if err := yaml.Unmarshal(config, &y); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %s", err)
}

_, ok := y["pki"]
if !ok {
return nil, fmt.Errorf("config is missing expected pki section")
}

_, ok = y["pki"].(map[any]any)
if !ok {
return nil, fmt.Errorf("config has unexpected value for pki section")
}

y["pki"].(map[any]any)["cert"] = string(cert)

return yaml.Marshal(y)
}

// FetchConfigPrivateKey takes a Nebula YAML, finds and returns its contained Nebula PEM-formatted private key,
// the Nebula PEM-formatted host cert, or an error.
func FetchConfigPrivateKeyAndCert(config []byte) ([]byte, []byte, error) {
var y map[any]any
if err := yaml.Unmarshal(config, &y); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal config: %s", err)
}

_, ok := y["pki"]
if !ok {
return nil, nil, fmt.Errorf("config is missing expected pki section")
}

pki, ok := y["pki"].(map[any]any)
if !ok {
return nil, nil, fmt.Errorf("config has unexpected value for pki section")
}

configKey, ok := pki["key"]
if !ok {
return nil, nil, fmt.Errorf("(%s) config is missing section 'key'", config)
}

existingKey, ok := configKey.(string)
if !ok {
return nil, nil, fmt.Errorf("config section 'key' found but has unexpected type: %T", configKey)
}

configCert, ok := pki["cert"]
if !ok {
return nil, nil, fmt.Errorf("config is missing 'cert' section")
}

existingCert, ok := configCert.(string)
if !ok {
return nil, nil, fmt.Errorf("config section 'cert' found but has unexpected type: %T", configCert)
}

return []byte(existingKey), []byte(existingCert), nil
}
27 changes: 24 additions & 3 deletions apiutil_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package dnapi

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
)
Expand All @@ -13,13 +15,32 @@ pki: {}
`), []byte("foobar"))
require.NoError(t, err)

var y map[string]interface{}
var y map[string]any
err = yaml.Unmarshal(cfg, &y)
require.NoError(t, err)

require.Equal(t, "foobar", y["pki"].(map[interface{}]interface{})["key"])
require.Equal(t, "foobar", y["pki"].(map[any]any)["key"])

cfg, err = InsertConfigPrivateKey([]byte(``), []byte("foobar"))
_, err = InsertConfigPrivateKey([]byte(``), []byte("foobar"))
require.Error(t, err)

}

func TestFetchConfigPrivateKey(t *testing.T) {
keyValue := []byte("foobar")
certValue := []byte("lolwat")

configValue := fmt.Sprintf(`pki: { cert: %s }`, certValue)
cfg, err := InsertConfigPrivateKey([]byte(configValue), keyValue)
require.NoError(t, err)

var y map[string]any
err = yaml.Unmarshal(cfg, &y)
require.NoError(t, err)
require.Equal(t, keyValue, []byte(y["pki"].(map[any]any)["key"].(string)))

fetchedVal, fetchedCert, err := FetchConfigPrivateKeyAndCert(cfg)
require.NoError(t, err)
assert.Equal(t, certValue, fetchedCert)
assert.Equal(t, keyValue, fetchedVal)
}
111 changes: 111 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,117 @@ func (c *Client) DoUpdate(ctx context.Context, creds keys.Credentials) ([]byte,
return result.Config, nebulaPrivkeyPEM, newCreds, meta, nil
}

// DoConfigUpdate sends a signed message to the DNClient API to fetch the new configuration update. During this call new keys
// are generated for DNClient API communication. If the API response is successful, the new configuration
// is returned along with the new DNClient API credentials and a meta object.
//
// See dnapi.InsertConfigPrivateKey and dnapi.InsertConfigCert for how to insert the old Nebula cert/private key into the configuration.
func (c *Client) DoConfigUpdate(ctx context.Context, creds keys.Credentials) ([]byte, *keys.Credentials, *ConfigMeta, error) {
// Rotate key
var hostPrivkey keys.PrivateKey // ECDSA

newKeys, err := keys.New()
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to generate new keys: %s", err)
}

msg := message.DoConfigUpdateRequest{
Nonce: nonce(),
}

// Set the correct keypair based on the current private key type
switch creds.PrivateKey.Unwrap().(type) {
case ed25519.PrivateKey:
hostPubkeyPEM, err := newKeys.HostEd25519PublicKey.MarshalPEM()
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to marshal Ed25519 public key: %s", err)
}
hostPrivkey = newKeys.HostEd25519PrivateKey
msg.HostPubkeyEd25519 = hostPubkeyPEM
case *ecdsa.PrivateKey:
hostPubkeyPEM, err := newKeys.HostP256PublicKey.MarshalPEM()
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to marshal P256 public key: %s", err)
}
hostPrivkey = newKeys.HostP256PrivateKey
msg.HostPubkeyP256 = hostPubkeyPEM
}

blob, err := json.Marshal(msg)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to marshal DNClient message: %s", err)
}

// Make API call
resp, err := c.postDNClient(ctx, message.DoConfigUpdate, blob, creds.HostID, creds.Counter, creds.PrivateKey)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to make API call to Defined Networking: %w", err)
}
resultWrapper := message.SignedResponseWrapper{}
err = json.Unmarshal(resp, &resultWrapper)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to unmarshal signed response wrapper: %s", err)
}

// Verify the signature
valid := false
for _, caPubkey := range creds.TrustedKeys {
if caPubkey.Verify(resultWrapper.Data.Message, resultWrapper.Data.Signature) {
valid = true
break
}
}
if !valid {
return nil, nil, nil, fmt.Errorf("failed to verify signed API result")
}

// Consume the verified message
result := message.DoConfigUpdateResponse{}
err = json.Unmarshal(resultWrapper.Data.Message, &result)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to unmarshal response (%s): %s", resultWrapper.Data.Message, err)
}

// Verify the nonce
if !bytes.Equal(result.Nonce, msg.Nonce) {
return nil, nil, nil, fmt.Errorf("nonce mismatch between request (%s) and response (%s)", msg.Nonce, result.Nonce)
}

// Verify the counter
if result.Counter <= creds.Counter {
return nil, nil, nil, fmt.Errorf("counter in request (%d) should be less than counter in response (%d)", creds.Counter, result.Counter)
}

trustedKeys, err := keys.TrustedKeysFromPEM(result.TrustedKeys)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to load trusted keys from bundle: %s", err)
}

newCreds := &keys.Credentials{
HostID: creds.HostID,
Counter: result.Counter,
PrivateKey: hostPrivkey,
TrustedKeys: trustedKeys,
}

meta := &ConfigMeta{
Org: ConfigOrg{
ID: result.Organization.ID,
Name: result.Organization.Name,
},
Network: ConfigNetwork{
ID: result.Network.ID,
Name: result.Network.Name,
},
Host: ConfigHost{
ID: result.Host.ID,
Name: result.Host.Name,
IPAddress: result.Host.IPAddress,
},
}

return result.Config, newCreds, meta, nil
}
func (c *Client) CommandResponse(ctx context.Context, creds keys.Credentials, responseToken string, response any) error {
value, err := json.Marshal(message.CommandResponseRequest{
ResponseToken: responseToken,
Expand Down
28 changes: 28 additions & 0 deletions dnapitest/dnapitest.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,34 @@ func (s *Server) handlerDNClient(w http.ResponseWriter, r *http.Request) {
return
}

case message.DoConfigUpdate:
var updateKeys message.DoConfigUpdateRequest
err = json.Unmarshal(msg.Value, &updateKeys)
if err != nil {
s.errors = append(s.errors, fmt.Errorf("failed to unmarshal DoUpdateRequest: %w", err))
http.Error(w, "failed to unmarshal DoUpdateRequest", http.StatusInternalServerError)
return
}

switch s.curve {
case message.NetworkCurve25519:
if err := s.SetEdPubkey(updateKeys.HostPubkeyEd25519); err != nil {
s.errors = append(s.errors, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
case message.NetworkCurveP256:
if err := s.SetP256Pubkey(updateKeys.HostPubkeyP256); err != nil {
s.errors = append(s.errors, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
default:
s.errors = append(s.errors, fmt.Errorf("invalid curve"))
http.Error(w, "invalid curve", http.StatusInternalServerError)
return
}

case message.LongPollWait:
var longPoll message.LongPollWaitRequest
err = json.Unmarshal(msg.Value, &longPoll)
Expand Down
19 changes: 19 additions & 0 deletions message/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
const (
CheckForUpdate = "CheckForUpdate"
DoUpdate = "DoUpdate"
DoConfigUpdate = "DoConfigUpdate"
LongPollWait = "LongPollWait"
CommandResponse = "CommandResponse"
Reauthenticate = "Reauthenticate"
Expand Down Expand Up @@ -83,6 +84,24 @@ type DoUpdateResponse struct {
EndpointOIDCMeta *HostEndpointOIDCMetadata `json:"endpointOIDC"`
}

// DoConfigUpdateRequest is the request sent for a DoConfigUpdate request.
type DoConfigUpdateRequest struct {
HostPubkeyEd25519 []byte `json:"edPubkeyPEM"` // X25519 (used for signing)
HostPubkeyP256 []byte `json:"p256HostPubkeyPEM"` // P256 (used for signing)
Nonce []byte `json:"nonce"`
}

// DoConfigUpdateResponse is the response generated for a DoConfigUpdate request.
type DoConfigUpdateResponse struct {
Config []byte `json:"config"`
Counter uint `json:"counter"`
Nonce []byte `json:"nonce"`
TrustedKeys []byte `json:"trustedKeys"`
Organization HostOrgMetadata `json:"organization"`
Network HostNetworkMetadata `json:"network"`
Host HostHostMetadata `json:"host"`
}

// LongPollWaitResponseWrapper contains a response to LongPollWait inside "data."
type LongPollWaitResponseWrapper struct {
Data LongPollWaitResponse `json:"data"`
Expand Down
Loading