Skip to content

Commit 90bd85d

Browse files
committed
Initial commit
0 parents  commit 90bd85d

14 files changed

Lines changed: 1463 additions & 0 deletions

File tree

Makefile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
vet:
2+
go vet -v ./...
3+
4+
fmt:
5+
go fmt ./...
6+
7+
test:
8+
$(TEST_ENV) go test $(TEST_FLAGS) $(shell go list ./...)
9+
10+
testv: TEST_FLAGS += -v
11+
testv: test
12+
13+
testvv: TEST_ENV += TEST_LOGS=1
14+
testvv: testv
15+
16+
testvvv: TEST_ENV += TEST_LOGS=2
17+
testvvv: testv
18+
19+
testvvvv: TEST_ENV += TEST_LOGS=3
20+
testvvvv: testv
21+
22+
.PHONY: vet fmt test testv testvv testvvv testvvvv

apiutil.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package dnapi
2+
3+
import (
4+
"fmt"
5+
6+
"gopkg.in/yaml.v2"
7+
)
8+
9+
// InsertConfigPrivateKey takes a Nebula YAML and a Nebula PEM-formatted private key, and inserts the private key into
10+
// the config, overwriting any previous value stored in the config.
11+
func InsertConfigPrivateKey(config []byte, privkey []byte) ([]byte, error) {
12+
var y map[interface{}]interface{}
13+
if err := yaml.Unmarshal(config, &y); err != nil {
14+
return nil, fmt.Errorf("failed to unmarshal config: %s", err)
15+
}
16+
17+
_, ok := y["pki"]
18+
if !ok {
19+
return nil, fmt.Errorf("config is missing expected pki section")
20+
}
21+
22+
_, ok = y["pki"].(map[interface{}]interface{})
23+
if !ok {
24+
return nil, fmt.Errorf("config has unexpected value for pki section")
25+
}
26+
27+
y["pki"].(map[interface{}]interface{})["key"] = string(privkey)
28+
29+
return yaml.Marshal(y)
30+
}

apiutil_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package dnapi
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
"gopkg.in/yaml.v2"
8+
)
9+
10+
func TestInsertConfigPrivateKey(t *testing.T) {
11+
cfg, err := InsertConfigPrivateKey([]byte(`
12+
pki: {}
13+
`), []byte("foobar"))
14+
require.NoError(t, err)
15+
16+
var y map[string]interface{}
17+
err = yaml.Unmarshal(cfg, &y)
18+
require.NoError(t, err)
19+
20+
require.Equal(t, "foobar", y["pki"].(map[interface{}]interface{})["key"])
21+
22+
cfg, err = InsertConfigPrivateKey([]byte(``), []byte("foobar"))
23+
require.Error(t, err)
24+
25+
}

client.go

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
// Package dnapi handles communication with the Defined Networking cloud API server.
2+
package dnapi
3+
4+
import (
5+
"bytes"
6+
"context"
7+
"crypto/ed25519"
8+
"encoding/base64"
9+
"encoding/json"
10+
"fmt"
11+
"io"
12+
"net/http"
13+
"time"
14+
15+
"github.com/DefinedNet/dnapi/message"
16+
"github.com/sirupsen/logrus"
17+
"github.com/slackhq/nebula/cert"
18+
)
19+
20+
// Client communicates with the API server.
21+
type Client struct {
22+
http *http.Client
23+
dnServer string
24+
}
25+
26+
// NewClient returns new Client configured with the given useragent.
27+
// It also supports reading Proxy information from the environment.
28+
func NewClient(useragent string, dnServer string) *Client {
29+
return &Client{
30+
http: &http.Client{
31+
Timeout: 1 * time.Minute,
32+
Transport: &uaTransport{
33+
T: &http.Transport{
34+
Proxy: http.ProxyFromEnvironment,
35+
},
36+
useragent: useragent,
37+
},
38+
},
39+
dnServer: dnServer,
40+
}
41+
}
42+
43+
// APIError contains an error, and a hidden wrapped error that contains the RequestID
44+
// contained in the X-Request-ID header of an API response. Defaults to empty string
45+
// if the header is not in the response.
46+
type APIError struct {
47+
e error
48+
ReqID string
49+
}
50+
51+
func (e *APIError) Error() string {
52+
return e.e.Error()
53+
}
54+
55+
func (e *APIError) Unwrap() error {
56+
return e.e
57+
}
58+
59+
type InvalidCredentialsError struct{}
60+
61+
func (e InvalidCredentialsError) Error() string {
62+
return "invalid credentials"
63+
}
64+
65+
type EnrollMeta struct {
66+
OrganizationID string
67+
OrganizationName string
68+
}
69+
70+
func (c *Client) EnrollWithTimeout(ctx context.Context, t time.Duration, logger logrus.FieldLogger, code string) ([]byte, []byte, *Credentials, *EnrollMeta, error) {
71+
toCtx, cancel := context.WithTimeout(ctx, t)
72+
defer cancel()
73+
return c.Enroll(toCtx, logger, code)
74+
}
75+
76+
// Enroll issues an enrollment request against the REST API using the given enrollment code, passing along a locally
77+
// generated DH X25519 public key to be signed by the CA, and an Ed 25519 public key for future API call authentication.
78+
// On success it returns the Nebula config generated by the server, a Nebula private key PEM to be inserted into the
79+
// config (see api.InsertConfigPrivateKey), credentials to be used in DNClient API requests, and a meta object
80+
// containing organization info.
81+
func (c *Client) Enroll(ctx context.Context, logger logrus.FieldLogger, code string) ([]byte, []byte, *Credentials, *EnrollMeta, error) {
82+
logger.WithFields(logrus.Fields{"server": c.dnServer}).Debug("Making enrollment request to API")
83+
84+
// Generate initial Ed25519 keypair for API communication
85+
dhPubkeyPEM, dhPrivkeyPEM, edPubkey, edPrivkey, err := newKeys()
86+
if err != nil {
87+
return nil, nil, nil, nil, err
88+
}
89+
90+
// Make a request to the API with the enrollment code
91+
jv, err := json.Marshal(message.EnrollRequest{
92+
Code: code,
93+
DHPubkey: dhPubkeyPEM,
94+
EdPubkey: cert.MarshalEd25519PublicKey(edPubkey),
95+
Timestamp: time.Now(),
96+
})
97+
if err != nil {
98+
return nil, nil, nil, nil, err
99+
}
100+
101+
req, err := http.NewRequestWithContext(ctx, "POST", c.dnServer+message.EnrollEndpoint, bytes.NewBuffer(jv))
102+
if err != nil {
103+
return nil, nil, nil, nil, err
104+
}
105+
106+
resp, err := c.http.Do(req)
107+
if err != nil {
108+
return nil, nil, nil, nil, err
109+
}
110+
defer resp.Body.Close()
111+
112+
// Log the request ID returned from the server
113+
reqID := resp.Header.Get("X-Request-ID")
114+
logger.WithFields(logrus.Fields{"reqID": reqID}).Info("Enrollment request complete")
115+
116+
// Decode the response
117+
r := message.EnrollResponse{}
118+
b, err := io.ReadAll(resp.Body)
119+
if err != nil {
120+
return nil, nil, nil, nil, &APIError{e: fmt.Errorf("error reading response body: %s", err), ReqID: reqID}
121+
}
122+
123+
if err := json.Unmarshal(b, &r); err != nil {
124+
return nil, nil, nil, nil, &APIError{e: fmt.Errorf("error decoding JSON response: %s\nbody: %s", err, b), ReqID: reqID}
125+
}
126+
127+
// Check for any errors returned by the API
128+
if err := r.Errors.ToError(); err != nil {
129+
return nil, nil, nil, nil, &APIError{e: fmt.Errorf("unexpected error during enrollment: %v", err), ReqID: reqID}
130+
}
131+
132+
meta := &EnrollMeta{
133+
OrganizationID: r.Data.Organization.ID,
134+
OrganizationName: r.Data.Organization.Name,
135+
}
136+
137+
trustedKeys, err := Ed25519PublicKeysFromPEM(r.Data.TrustedKeys)
138+
if err != nil {
139+
return nil, nil, nil, nil, &APIError{e: fmt.Errorf("failed to load trusted keys from bundle: %s", err), ReqID: reqID}
140+
}
141+
142+
creds := &Credentials{
143+
HostID: r.Data.HostID,
144+
PrivateKey: edPrivkey,
145+
Counter: r.Data.Counter,
146+
TrustedKeys: trustedKeys,
147+
}
148+
return r.Data.Config, dhPrivkeyPEM, creds, meta, nil
149+
}
150+
151+
func (c *Client) CheckForUpdateWithTimeout(ctx context.Context, t time.Duration, creds Credentials) (bool, error) {
152+
toCtx, cancel := context.WithTimeout(ctx, t)
153+
defer cancel()
154+
return c.CheckForUpdate(toCtx, creds)
155+
}
156+
157+
// CheckForUpdate sends a signed message to the DNClient API to learn if there is a new configuration available.
158+
func (c *Client) CheckForUpdate(ctx context.Context, creds Credentials) (bool, error) {
159+
respBody, err := c.postDNClient(ctx, message.CheckForUpdate, nil, creds.HostID, creds.Counter, creds.PrivateKey)
160+
if err != nil {
161+
return false, fmt.Errorf("failed to post message to dnclient api: %w", err)
162+
}
163+
result := message.CheckForUpdateResponseWrapper{}
164+
err = json.Unmarshal(respBody, &result)
165+
if err != nil {
166+
return false, fmt.Errorf("failed to interpret API response: %s", err)
167+
}
168+
return result.Data.UpdateAvailable, nil
169+
}
170+
171+
func (c *Client) DoUpdateWithTimeout(ctx context.Context, t time.Duration, creds Credentials) ([]byte, []byte, *Credentials, error) {
172+
toCtx, cancel := context.WithTimeout(ctx, t)
173+
defer cancel()
174+
return c.DoUpdate(toCtx, creds)
175+
}
176+
177+
// DoUpdate sends a signed message to the DNClient API to fetch the new configuration update. During this call a new
178+
// DH X25519 keypair is generated for the new Nebula certificate as well as a new Ed25519 keypair for DNClient API
179+
// communication. On success it returns the new config, a Nebula private key PEM to be inserted into the config (see
180+
// api.InsertConfigPrivateKey) and new DNClient API credentials.
181+
func (c *Client) DoUpdate(ctx context.Context, creds Credentials) ([]byte, []byte, *Credentials, error) {
182+
// Rotate keys
183+
dhPubkeyPEM, dhPrivkeyPEM, edPubkey, edPrivkey, err := newKeys()
184+
if err != nil {
185+
return nil, nil, nil, err
186+
}
187+
188+
updateKeys := message.DoUpdateRequest{
189+
EdPubkeyPEM: cert.MarshalEd25519PublicKey(edPubkey),
190+
DHPubkeyPEM: dhPubkeyPEM,
191+
Nonce: nonce(),
192+
}
193+
194+
updateKeysBlob, err := json.Marshal(updateKeys)
195+
if err != nil {
196+
return nil, nil, nil, fmt.Errorf("failed to marshal DNClient message: %s", err)
197+
}
198+
199+
// Make API call
200+
resp, err := c.postDNClient(ctx, message.DoUpdate, updateKeysBlob, creds.HostID, creds.Counter, creds.PrivateKey)
201+
if err != nil {
202+
return nil, nil, nil, fmt.Errorf("failed to make API call to Defined Networking: %w", err)
203+
}
204+
resultWrapper := message.SignedResponseWrapper{}
205+
err = json.Unmarshal(resp, &resultWrapper)
206+
if err != nil {
207+
return nil, nil, nil, fmt.Errorf("failed to unmarshal signed response wrapper: %s", err)
208+
}
209+
210+
// Verify the signature
211+
valid := false
212+
for _, caPubkey := range creds.TrustedKeys {
213+
if ed25519.Verify(caPubkey, resultWrapper.Data.Message, resultWrapper.Data.Signature) {
214+
valid = true
215+
break
216+
}
217+
}
218+
if !valid {
219+
return nil, nil, nil, fmt.Errorf("failed to verify signed API result")
220+
}
221+
222+
// Consume the verified message
223+
result := message.DoUpdateResponse{}
224+
err = json.Unmarshal(resultWrapper.Data.Message, &result)
225+
if err != nil {
226+
return nil, nil, nil, fmt.Errorf("failed to unmarshal response (%s): %s", resultWrapper.Data.Message, err)
227+
}
228+
229+
// Verify the nonce
230+
if !bytes.Equal(result.Nonce, updateKeys.Nonce) {
231+
return nil, nil, nil, fmt.Errorf("nonce mismatch between request (%s) and response (%s)", updateKeys.Nonce, result.Nonce)
232+
}
233+
234+
// Verify the counter
235+
if result.Counter <= creds.Counter {
236+
return nil, nil, nil, fmt.Errorf("counter in request (%d) should be less than counter in response (%d)", creds.Counter, result.Counter)
237+
}
238+
239+
trustedKeys, err := Ed25519PublicKeysFromPEM(result.TrustedKeys)
240+
if err != nil {
241+
return nil, nil, nil, fmt.Errorf("failed to load trusted keys from bundle: %s", err)
242+
}
243+
244+
newCreds := &Credentials{
245+
HostID: creds.HostID,
246+
Counter: result.Counter,
247+
PrivateKey: edPrivkey,
248+
TrustedKeys: trustedKeys,
249+
}
250+
251+
return result.Config, dhPrivkeyPEM, newCreds, nil
252+
}
253+
254+
// postDNClient wraps and signs the given dnclientRequestWrapper message, and makes the API call.
255+
// On success, it returns the response message body. On error, the error is returned.
256+
func (c *Client) postDNClient(ctx context.Context, reqType string, value []byte, hostID string, counter uint, privkey ed25519.PrivateKey) ([]byte, error) {
257+
encMsg, err := json.Marshal(message.RequestWrapper{
258+
Type: reqType,
259+
Value: value,
260+
Timestamp: time.Now(),
261+
})
262+
if err != nil {
263+
return nil, err
264+
}
265+
signedMsg := base64.StdEncoding.EncodeToString(encMsg)
266+
sig := ed25519.Sign(privkey, []byte(signedMsg))
267+
body := message.RequestV1{
268+
Version: 1,
269+
HostID: hostID,
270+
Counter: counter,
271+
Message: signedMsg,
272+
Signature: sig,
273+
}
274+
postBody, err := json.Marshal(body)
275+
if err != nil {
276+
return nil, err
277+
}
278+
req, err := http.NewRequestWithContext(ctx, "POST", c.dnServer+message.EndpointV1, bytes.NewReader(postBody))
279+
if err != nil {
280+
return nil, err
281+
}
282+
resp, err := c.http.Do(req)
283+
if err != nil {
284+
return nil, fmt.Errorf("failed to call dnclient endpoint: %s", err)
285+
}
286+
defer resp.Body.Close()
287+
288+
respBody, err := io.ReadAll(resp.Body)
289+
if err != nil {
290+
return nil, fmt.Errorf("failed to read the response body: %s", err)
291+
}
292+
293+
switch resp.StatusCode {
294+
case http.StatusOK:
295+
return respBody, nil
296+
case http.StatusUnauthorized:
297+
return nil, InvalidCredentialsError{}
298+
default:
299+
var errors struct {
300+
Errors message.APIErrors
301+
}
302+
if err := json.Unmarshal(respBody, &errors); err != nil {
303+
return nil, fmt.Errorf("dnclient endpoint returned bad status code '%d', body: %s", resp.StatusCode, respBody)
304+
}
305+
return nil, errors.Errors.ToError()
306+
}
307+
}
308+
309+
type uaTransport struct {
310+
useragent string
311+
T http.RoundTripper
312+
}
313+
314+
func (t *uaTransport) RoundTrip(req *http.Request) (*http.Response, error) {
315+
req.Header.Set("User-Agent", t.useragent)
316+
return t.T.RoundTrip(req)
317+
}

0 commit comments

Comments
 (0)