diff --git a/client.go b/client.go index b5285d3..f14700a 100644 --- a/client.go +++ b/client.go @@ -85,17 +85,33 @@ func (e InvalidCredentialsError) Error() string { return "invalid credentials" } -type EnrollMeta struct { - OrganizationID string - OrganizationName string +type ConfigMeta struct { + Org ConfigOrg + Network ConfigNetwork + Host ConfigHost +} + +type ConfigOrg struct { + ID string + Name string +} + +type ConfigNetwork struct { + ID string + Name string +} + +type ConfigHost struct { + ID string + Name string + IPAddress string } // Enroll issues an enrollment request against the REST API using the given enrollment code, passing along a locally // generated DH X25519 public key to be signed by the CA, and an Ed 25519 public key for future API call authentication. // On success it returns the Nebula config generated by the server, a Nebula private key PEM to be inserted into the -// config (see api.InsertConfigPrivateKey), credentials to be used in DNClient API requests, and a meta object -// containing organization info. -func (c *Client) Enroll(ctx context.Context, logger logrus.FieldLogger, code string) ([]byte, []byte, *keys.Credentials, *EnrollMeta, error) { +// config (see api.InsertConfigPrivateKey), credentials to be used in DNClient API requests, and a meta object. +func (c *Client) Enroll(ctx context.Context, logger logrus.FieldLogger, code string) ([]byte, []byte, *keys.Credentials, *ConfigMeta, error) { logger.WithFields(logrus.Fields{"server": c.dnServer}).Debug("Making enrollment request to API") // Generate newKeys for the enrollment request @@ -168,9 +184,20 @@ func (c *Client) Enroll(ctx context.Context, logger logrus.FieldLogger, code str return nil, nil, nil, nil, &APIError{e: fmt.Errorf("unexpected error during enrollment: %v", err), ReqID: reqID} } - meta := &EnrollMeta{ - OrganizationID: r.Data.Organization.ID, - OrganizationName: r.Data.Organization.Name, + meta := &ConfigMeta{ + Org: ConfigOrg{ + ID: r.Data.Organization.ID, + Name: r.Data.Organization.Name, + }, + Network: ConfigNetwork{ + ID: r.Data.Network.ID, + Name: r.Data.Network.Name, + }, + Host: ConfigHost{ + ID: r.Data.HostID, + Name: r.Data.Host.Name, + IPAddress: r.Data.Host.IPAddress, + }, } // Determine the private keys to save based on the network curve type @@ -239,17 +266,17 @@ func (c *Client) LongPollWait(ctx context.Context, creds keys.Credentials, suppo // DoUpdate sends a signed message to the DNClient API to fetch the new configuration update. During this call new keys // are generated both for Nebula and DNClient API communication. If the API response is successful, the new configuration -// is returned along with the new Nebula private key PEM and new DNClient API credentials. +// is returned along with the new Nebula private key PEM, new DNClient API credentials, and a meta object. // // See dnapi.InsertConfigPrivateKey for how to insert the new Nebula private key into the configuration. -func (c *Client) DoUpdate(ctx context.Context, creds keys.Credentials) ([]byte, []byte, *keys.Credentials, error) { +func (c *Client) DoUpdate(ctx context.Context, creds keys.Credentials) ([]byte, []byte, *keys.Credentials, *ConfigMeta, error) { // Rotate keys var nebulaPrivkeyPEM []byte // ECDH 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) + return nil, nil, nil, nil, fmt.Errorf("failed to generate new keys: %s", err) } msg := message.DoUpdateRequest{ @@ -261,7 +288,7 @@ func (c *Client) DoUpdate(ctx context.Context, creds keys.Credentials) ([]byte, 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) + return nil, nil, nil, nil, fmt.Errorf("failed to marshal Ed25519 public key: %s", err) } hostPrivkey = newKeys.HostEd25519PrivateKey nebulaPrivkeyPEM = newKeys.NebulaX25519PrivateKeyPEM @@ -270,7 +297,7 @@ func (c *Client) DoUpdate(ctx context.Context, creds keys.Credentials) ([]byte, 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) + return nil, nil, nil, nil, fmt.Errorf("failed to marshal P256 public key: %s", err) } hostPrivkey = newKeys.HostP256PrivateKey nebulaPrivkeyPEM = newKeys.NebulaP256PrivateKeyPEM @@ -280,18 +307,18 @@ func (c *Client) DoUpdate(ctx context.Context, creds keys.Credentials) ([]byte, blob, err := json.Marshal(msg) if err != nil { - return nil, nil, nil, fmt.Errorf("failed to marshal DNClient message: %s", err) + return nil, nil, nil, nil, fmt.Errorf("failed to marshal DNClient message: %s", err) } // Make API call resp, err := c.postDNClient(ctx, message.DoUpdate, 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) + return nil, 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) + return nil, nil, nil, nil, fmt.Errorf("failed to unmarshal signed response wrapper: %s", err) } // Verify the signature @@ -303,29 +330,29 @@ func (c *Client) DoUpdate(ctx context.Context, creds keys.Credentials) ([]byte, } } if !valid { - return nil, nil, nil, fmt.Errorf("failed to verify signed API result") + return nil, nil, nil, nil, fmt.Errorf("failed to verify signed API result") } // Consume the verified message result := message.DoUpdateResponse{} 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) + return nil, 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) + return nil, 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) + return nil, 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) + return nil, nil, nil, nil, fmt.Errorf("failed to load trusted keys from bundle: %s", err) } newCreds := &keys.Credentials{ @@ -335,7 +362,23 @@ func (c *Client) DoUpdate(ctx context.Context, creds keys.Credentials) ([]byte, TrustedKeys: trustedKeys, } - return result.Config, nebulaPrivkeyPEM, newCreds, nil + 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, nebulaPrivkeyPEM, newCreds, meta, nil } func (c *Client) CommandResponse(ctx context.Context, creds keys.Credentials, responseToken string, response any) error { diff --git a/client_test.go b/client_test.go index 6be7a92..4860b7f 100644 --- a/client_test.go +++ b/client_test.go @@ -41,11 +41,15 @@ func TestEnroll(t *testing.T) { // Happy path enrollment code := "abcdef" - hostID := "foobar" orgID := "foobaz" orgName := "foobar's foo org" netID := "qux" + netName := "the best network" netCurve := message.NetworkCurve25519 + netCIDR := "192.168.100.0/24" + hostID := "foobar" + hostName := "foo host" + hostIP := "192.168.100.1" counter := uint(5) ca, _ := dnapitest.NebulaCACert() caPEM, err := ca.MarshalToPEM() @@ -73,13 +77,20 @@ func TestEnroll(t *testing.T) { Counter: counter, Config: cfg, TrustedKeys: marshalCAPublicKey(ca.Details.Curve, ca.Details.PublicKey), - Organization: message.EnrollResponseDataOrg{ + Organization: message.HostOrgMetadata{ ID: orgID, Name: orgName, }, - Network: message.EnrollResponseDataNetwork{ + Network: message.HostNetworkMetadata{ ID: netID, + Name: netName, Curve: netCurve, + CIDR: netCIDR, + }, + Host: message.HostHostMetadata{ + ID: hostID, + Name: hostName, + IPAddress: hostIP, }, }, }) @@ -121,8 +132,13 @@ func TestEnroll(t *testing.T) { assert.Empty(t, y.PKI.Key) // test meta - assert.Equal(t, orgID, meta.OrganizationID) - assert.Equal(t, orgName, meta.OrganizationName) + assert.Equal(t, orgID, meta.Org.ID) + assert.Equal(t, orgName, meta.Org.Name) + assert.Equal(t, netID, meta.Network.ID) + assert.Equal(t, netName, meta.Network.Name) + assert.Equal(t, hostID, meta.Host.ID) + assert.Equal(t, hostName, meta.Host.Name) + assert.Equal(t, hostIP, meta.Host.IPAddress) // Test error handling errorMsg := "invalid enrollment code" @@ -186,13 +202,20 @@ func TestDoUpdate(t *testing.T) { Counter: 1, Config: cfg, TrustedKeys: marshalCAPublicKey(ca.Details.Curve, ca.Details.PublicKey), - Organization: message.EnrollResponseDataOrg{ + Organization: message.HostOrgMetadata{ ID: "foobaz", Name: "foobar's foo org", }, - Network: message.EnrollResponseDataNetwork{ + Network: message.HostNetworkMetadata{ ID: "qux", + Name: "the best network", Curve: message.NetworkCurve25519, + CIDR: "192.168.100.0/24", + }, + Host: message.HostHostMetadata{ + ID: "quux", + Name: "foo host", + IPAddress: "192.168.100.2", }, }, }) @@ -245,9 +268,25 @@ func TestDoUpdate(t *testing.T) { // Invalid signature ts.ExpectRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { newConfigResponse := message.DoUpdateResponse{ - Config: dnapitest.NebulaCfg(caPEM), - Counter: 2, - Nonce: dnapitest.GetNonce(r), + Config: dnapitest.NebulaCfg(caPEM), + Counter: 2, + Nonce: dnapitest.GetNonce(r), + TrustedKeys: marshalCAPublicKey(ca.Details.Curve, ca.Details.PublicKey), + Organization: message.HostOrgMetadata{ + ID: "foobaz", + Name: "foobar's foo org", + }, + Network: message.HostNetworkMetadata{ + ID: "qux", + Name: "the best network", + Curve: message.NetworkCurve25519, + CIDR: "192.168.100.0/24", + }, + Host: message.HostHostMetadata{ + ID: "quux", + Name: "foo host", + IPAddress: "192.168.100.2", + }, } rawRes := jsonMarshal(newConfigResponse) @@ -273,7 +312,7 @@ func TestDoUpdate(t *testing.T) { ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - cfg, pkey, newCreds, err := c.DoUpdate(ctx, *creds) + cfg, pkey, newCreds, _, err := c.DoUpdate(ctx, *creds) require.Error(t, err) assert.Empty(t, ts.Errors()) assert.Equal(t, 0, ts.RequestsRemaining()) @@ -284,9 +323,25 @@ func TestDoUpdate(t *testing.T) { // Invalid counter ts.ExpectRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { newConfigResponse := message.DoUpdateResponse{ - Config: dnapitest.NebulaCfg(caPEM), - Counter: 0, - Nonce: dnapitest.GetNonce(r), + Config: dnapitest.NebulaCfg(caPEM), + Counter: 0, + Nonce: dnapitest.GetNonce(r), + TrustedKeys: marshalCAPublicKey(ca.Details.Curve, ca.Details.PublicKey), + Organization: message.HostOrgMetadata{ + ID: "foobaz", + Name: "foobar's foo org", + }, + Network: message.HostNetworkMetadata{ + ID: "qux", + Name: "the best network", + Curve: message.NetworkCurve25519, + CIDR: "192.168.100.0/24", + }, + Host: message.HostHostMetadata{ + ID: "quux", + Name: "foo host", + IPAddress: "192.168.100.2", + }, } rawRes := jsonMarshal(newConfigResponse) @@ -306,7 +361,7 @@ func TestDoUpdate(t *testing.T) { ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - cfg, pkey, newCreds, err = c.DoUpdate(ctx, *creds) + cfg, pkey, newCreds, _, err = c.DoUpdate(ctx, *creds) require.Error(t, err) assert.Empty(t, ts.Errors()) assert.Equal(t, 0, ts.RequestsRemaining()) @@ -314,12 +369,38 @@ func TestDoUpdate(t *testing.T) { require.Nil(t, cfg) require.Nil(t, pkey) + orgID := "foobaz" + orgName := "foobar's foo org" + netID := "qux" + netName := "the best network" + netCurve := message.NetworkCurve25519 + netCIDR := "192.168.100.0/24" + hostID := "foobar" + hostName := "foo host" + hostIP := "192.168.100.1" + // This time sign the response with the correct CA key. ts.ExpectRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { newConfigResponse := message.DoUpdateResponse{ - Config: dnapitest.NebulaCfg(caPEM), - Counter: 3, - Nonce: dnapitest.GetNonce(r), + Config: dnapitest.NebulaCfg(caPEM), + Counter: 3, + Nonce: dnapitest.GetNonce(r), + TrustedKeys: marshalCAPublicKey(ca.Details.Curve, ca.Details.PublicKey), + Organization: message.HostOrgMetadata{ + ID: orgID, + Name: orgName, + }, + Network: message.HostNetworkMetadata{ + ID: netID, + Name: netName, + Curve: netCurve, + CIDR: netCIDR, + }, + Host: message.HostHostMetadata{ + ID: hostID, + Name: hostName, + IPAddress: hostIP, + }, } rawRes := jsonMarshal(newConfigResponse) @@ -334,11 +415,20 @@ func TestDoUpdate(t *testing.T) { ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - _, _, _, err = c.DoUpdate(ctx, *creds) + _, _, _, meta, err := c.DoUpdate(ctx, *creds) require.NoError(t, err) assert.Empty(t, ts.Errors()) assert.Equal(t, 0, ts.RequestsRemaining()) + // test meta + assert.Equal(t, orgID, meta.Org.ID) + assert.Equal(t, orgName, meta.Org.Name) + assert.Equal(t, netID, meta.Network.ID) + assert.Equal(t, netName, meta.Network.Name) + assert.Equal(t, hostID, meta.Host.ID) + assert.Equal(t, hostName, meta.Host.Name) + assert.Equal(t, hostIP, meta.Host.IPAddress) + } func TestDoUpdate_P256(t *testing.T) { @@ -377,13 +467,20 @@ func TestDoUpdate_P256(t *testing.T) { Counter: 1, Config: cfg, TrustedKeys: marshalCAPublicKey(ca.Details.Curve, ca.Details.PublicKey), - Organization: message.EnrollResponseDataOrg{ + Organization: message.HostOrgMetadata{ ID: "foobaz", Name: "foobar's foo org", }, - Network: message.EnrollResponseDataNetwork{ + Network: message.HostNetworkMetadata{ ID: "qux", + Name: "the best network", Curve: message.NetworkCurveP256, + CIDR: "192.168.100.0/24", + }, + Host: message.HostHostMetadata{ + ID: "quux", + Name: "foo host", + IPAddress: "192.168.100.2", }, }, }) @@ -470,7 +567,7 @@ func TestDoUpdate_P256(t *testing.T) { ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - cfg, pkey, newCreds, err := c.DoUpdate(ctx, *creds) + cfg, pkey, newCreds, _, err := c.DoUpdate(ctx, *creds) require.Error(t, err) assert.Empty(t, ts.Errors()) assert.Equal(t, 0, ts.RequestsRemaining()) @@ -514,7 +611,7 @@ func TestDoUpdate_P256(t *testing.T) { ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - cfg, pkey, newCreds, err = c.DoUpdate(ctx, *creds) + cfg, pkey, newCreds, _, err = c.DoUpdate(ctx, *creds) require.Error(t, err) assert.Empty(t, ts.Errors()) assert.Equal(t, 0, ts.RequestsRemaining()) @@ -525,9 +622,25 @@ func TestDoUpdate_P256(t *testing.T) { // This time sign the response with the correct CA key. ts.ExpectRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { newConfigResponse := message.DoUpdateResponse{ - Config: dnapitest.NebulaCfg(caPEM), - Counter: 3, - Nonce: dnapitest.GetNonce(r), + Config: dnapitest.NebulaCfg(caPEM), + Counter: 3, + Nonce: dnapitest.GetNonce(r), + TrustedKeys: marshalCAPublicKey(ca.Details.Curve, ca.Details.PublicKey), + Organization: message.HostOrgMetadata{ + ID: "foobaz", + Name: "foobar's foo org", + }, + Network: message.HostNetworkMetadata{ + ID: "qux", + Name: "the best network", + Curve: message.NetworkCurve25519, + CIDR: "192.168.100.0/24", + }, + Host: message.HostHostMetadata{ + ID: "quux", + Name: "foo host", + IPAddress: "192.168.100.2", + }, } rawRes := jsonMarshal(newConfigResponse) hashed := sha256.Sum256(rawRes) @@ -552,7 +665,7 @@ func TestDoUpdate_P256(t *testing.T) { ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - _, _, _, err = c.DoUpdate(ctx, *creds) + _, _, _, _, err = c.DoUpdate(ctx, *creds) require.NoError(t, err) assert.Empty(t, ts.Errors()) assert.Equal(t, 0, ts.RequestsRemaining()) @@ -595,13 +708,20 @@ func TestCommandResponse(t *testing.T) { Counter: 1, Config: cfg, TrustedKeys: marshalCAPublicKey(ca.Details.Curve, ca.Details.PublicKey), - Organization: message.EnrollResponseDataOrg{ + Organization: message.HostOrgMetadata{ ID: "foobaz", Name: "foobar's foo org", }, - Network: message.EnrollResponseDataNetwork{ + Network: message.HostNetworkMetadata{ ID: "qux", + Name: "the best network", Curve: message.NetworkCurve25519, + CIDR: "192.168.100.0/24", + }, + Host: message.HostHostMetadata{ + ID: "quux", + Name: "foo host", + IPAddress: "192.168.100.2", }, }, }) @@ -690,13 +810,20 @@ func TestStreamCommandResponse(t *testing.T) { Counter: 1, Config: cfg, TrustedKeys: marshalCAPublicKey(ca.Details.Curve, ca.Details.PublicKey), - Organization: message.EnrollResponseDataOrg{ + Organization: message.HostOrgMetadata{ ID: "foobaz", Name: "foobar's foo org", }, - Network: message.EnrollResponseDataNetwork{ + Network: message.HostNetworkMetadata{ ID: "qux", + Name: "the best network", Curve: message.NetworkCurve25519, + CIDR: "192.168.100.0/24", + }, + Host: message.HostHostMetadata{ + ID: "quux", + Name: "foo host", + IPAddress: "192.168.100.2", }, }, }) diff --git a/examples/simple/main.go b/examples/simple/main.go index a5e31a3..a1889f2 100644 --- a/examples/simple/main.go +++ b/examples/simple/main.go @@ -39,8 +39,8 @@ func main() { fmt.Printf( "Host ID: %s (Org: %s, ID: %s), Counter: %d, Config:\n\n%s\n", creds.HostID, - meta.OrganizationName, - meta.OrganizationID, + meta.Org.Name, + meta.Org.ID, creds.Counter, config, ) @@ -61,7 +61,7 @@ func main() { // be careful not to blow away creds in case err != nil // another option is to pass credentials by reference and let DoUpdate modify the struct if successful but // this makes it less obvious to the caller that they need to save the new credentials to disk - config, pkey, newCreds, err := c.DoUpdate(context.Background(), *creds) + config, pkey, newCreds, meta, err := c.DoUpdate(context.Background(), *creds) if err != nil { logger.WithError(err).Error("Failed to perform update") continue @@ -74,7 +74,7 @@ func main() { creds = newCreds - fmt.Printf("Counter: %d, config:\n\n%s\n", creds.Counter, config) + fmt.Printf("Counter: %d, config:\n\n%s\nmeta:\n%+v\n", creds.Counter, config, meta) // XXX Now would be a good time to save both the new config and credentials to disk and reload Nebula. } diff --git a/message/message.go b/message/message.go index d8b0227..ea9a5bd 100644 --- a/message/message.go +++ b/message/message.go @@ -72,10 +72,13 @@ type DoUpdateRequest struct { // DoUpdateResponse is the response generated for a DoUpdate request. type DoUpdateResponse struct { - Config []byte `json:"config"` - Counter uint `json:"counter"` - Nonce []byte `json:"nonce"` - TrustedKeys []byte `json:"trustedKeys"` + 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." @@ -138,24 +141,34 @@ type EnrollResponse struct { // EnrollResponseData is included in the EnrollResponse. type EnrollResponseData struct { - Config []byte `json:"config"` - HostID string `json:"hostID"` - Counter uint `json:"counter"` - TrustedKeys []byte `json:"trustedKeys"` - Organization EnrollResponseDataOrg `json:"organization"` - Network EnrollResponseDataNetwork `json:"network"` + Config []byte `json:"config"` + HostID string `json:"hostID"` + Counter uint `json:"counter"` + TrustedKeys []byte `json:"trustedKeys"` + Organization HostOrgMetadata `json:"organization"` + Network HostNetworkMetadata `json:"network"` + Host HostHostMetadata `json:"host"` } -// EnrollResponseDataOrg is included in EnrollResponseData. -type EnrollResponseDataOrg struct { +// HostOrgMetadata is included in EnrollResponseData. +type HostOrgMetadata struct { ID string `json:"id"` Name string `json:"name"` } -// EnrollResponseDataNetwork is included in EnrollResponseData. -type EnrollResponseDataNetwork struct { +// HostNetworkMetadata is included in EnrollResponseData. +type HostNetworkMetadata struct { ID string `json:"id"` + Name string `json:"name"` Curve NetworkCurve `json:"curve"` + CIDR string `json:"string"` +} + +// HostHostMetadata is included in EnrollResponseData. +type HostHostMetadata struct { + ID string `json:"id"` + Name string `json:"name"` + IPAddress string `json:"ipAddress"` } // APIError represents a single error returned in an API error response.