Skip to content

Commit

Permalink
add support for Secret Store client keys
Browse files Browse the repository at this point in the history
This lets clients create a client key for local encryption of secrets
before uploading to the Fastly API.

This bumps the minimum required Go version to 1.18 and introduces
`golang.org/x/crypto` as a dependency.
  • Loading branch information
joeshaw committed Feb 6, 2023
1 parent ef4694a commit d50d58c
Show file tree
Hide file tree
Showing 10 changed files with 369 additions and 15 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/pr_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.16.x
go-version: 1.18.x
- name: Restore cache
id: cache
uses: actions/cache@v2
Expand Down Expand Up @@ -46,7 +46,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.16.x]
go-version: [1.18.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
version: 1
interactions:
- request:
body: ""
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- FastlyGo/7.1.0 (+github.com/fastly/go-fastly; go1.19.5)
url: https://api.fastly.com/resources/stores/secret/client-key
method: POST
response:
body: |
{
"public_key": "CUBQitcJt9a4Vqem5vPYubm/jjZ6Inqr4gr/6aJukSc=",
"signature": "8ajkjOwcsUIQRS1Blsoqk3vL4gelCh1rqYZATzLGXumFB2VNCFhTgMzmV6ypPQ6VJcdtbUr/fEvGtHfagNH4DA==",
"expires_at": "2023-02-03T22:36:31.563174Z"
}
headers:
Access-Control-Allow-Headers:
- Content-Type, Fastly-Key
Access-Control-Allow-Methods:
- PUT, POST, GET, OPTIONS, DELETE
Access-Control-Allow-Origin:
- '*'
Content-Length:
- "221"
Content-Type:
- application/json
Date:
- Fri, 03 Feb 2023 22:26:31 GMT
Fastly-Trace-Id:
- vIOfYP6o8Rj6SVaJ2HkeQv
status: 200 OK
code: 200
duration: ""
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
version: 1
interactions:
- request:
body: |
{"name":"TestClient_CreateSecret_clientEncryption","secret":"cRol5IDLTry79axbRcblj0+JLrEuEPdfN715c6C7cADybGL+34DotXLX9IKgsx2Et7nfBFLxUtPlsoLNyMzUBg==","client_key":"CUBQitcJt9a4Vqem5vPYubm/jjZ6Inqr4gr/6aJukSc="}
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- FastlyGo/7.1.0 (+github.com/fastly/go-fastly; go1.19.5)
url: https://api.fastly.com/resources/stores/secret/gyxBrAO5eUjZMNwb3z93gn/secrets
method: POST
response:
body: |
{
"name": "TestClient_CreateSecret_clientEncryption",
"digest": "5W22wlDwhwa7dZ4y1+2ZbIZxDVibHHKordZCehpYvak=",
"created_at": "2023-02-03T22:26:31Z"
}
headers:
Access-Control-Allow-Headers:
- Content-Type, Fastly-Key
Access-Control-Allow-Methods:
- PUT, POST, GET, OPTIONS, DELETE
Access-Control-Allow-Origin:
- '*'
Content-Length:
- "157"
Content-Type:
- application/json
Date:
- Fri, 03 Feb 2023 22:26:31 GMT
Fastly-Trace-Id:
- nRfqFSFUBgKtWbp0VaFINo
status: 200 OK
code: 200
duration: ""
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
version: 1
interactions:
- request:
body: |
{"name":"TestClient_CreateSecret_clientEncryption-00"}
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- FastlyGo/7.1.0 (+github.com/fastly/go-fastly; go1.19.5)
url: https://api.fastly.com/resources/stores/secret
method: POST
response:
body: |
{
"id": "gyxBrAO5eUjZMNwb3z93gn",
"name": "TestClient_CreateSecret_clientEncryption-00",
"created_at": "2023-02-03T22:26:29Z"
}
headers:
Access-Control-Allow-Headers:
- Content-Type, Fastly-Key
Access-Control-Allow-Methods:
- PUT, POST, GET, OPTIONS, DELETE
Access-Control-Allow-Origin:
- '*'
Content-Length:
- "134"
Content-Type:
- application/json
Date:
- Fri, 03 Feb 2023 22:26:31 GMT
Fastly-Trace-Id:
- 3MNCmQuMyIl0ToFR25GtOK
status: 201 Created
code: 201
duration: ""
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
version: 1
interactions:
- request:
body: ""
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- FastlyGo/7.1.0 (+github.com/fastly/go-fastly; go1.19.5)
url: https://api.fastly.com/resources/stores/secret/gyxBrAO5eUjZMNwb3z93gn
method: DELETE
response:
body: ""
headers:
Access-Control-Allow-Headers:
- Content-Type, Fastly-Key
Access-Control-Allow-Methods:
- PUT, POST, GET, OPTIONS, DELETE
Access-Control-Allow-Origin:
- '*'
Date:
- Fri, 03 Feb 2023 22:26:31 GMT
Fastly-Trace-Id:
- c7kZMF2OBn2WeFQ9qV6TQY
status: 204 No Content
code: 204
duration: ""
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
version: 1
interactions:
- request:
body: ""
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- FastlyGo/7.1.0 (+github.com/fastly/go-fastly; go1.19.5)
url: https://api.fastly.com/resources/stores/secret/signing-key
method: GET
response:
body: |
{
"signing_key": "3Tb5p7bVMKg8TrhtmjB73d+A99yVNLR1Tfi+YwJTMw0="
}
headers:
Access-Control-Allow-Headers:
- Content-Type, Fastly-Key
Access-Control-Allow-Methods:
- PUT, POST, GET, OPTIONS, DELETE
Access-Control-Allow-Origin:
- '*'
Content-Length:
- "68"
Content-Type:
- application/json
Date:
- Fri, 03 Feb 2023 22:26:31 GMT
Etag:
- '"c2b40427846401e6a808f1a43a809a65770df02c49b3fc30aacf98e3aec0d515"'
Fastly-Trace-Id:
- wylcGg8FFv5zudofQ3PdXf
Surrogate-Control:
- max-age=86400, stale-if-error=86400
status: 200 OK
code: 200
duration: ""
87 changes: 83 additions & 4 deletions fastly/secret_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fastly

import (
"bytes"
"crypto/ed25519"
"encoding/json"
"strconv"
"time"
Expand Down Expand Up @@ -202,6 +203,8 @@ type CreateSecretInput struct {
// The value will be base64-encoded when delivered to the API, which is the
// required format.
Secret []byte
// ClientKey is the public key used to encrypt the secret with (optional).
ClientKey []byte
}

// CreateSecret creates a new resource.
Expand All @@ -220,11 +223,13 @@ func (c *Client) CreateSecret(i *CreateSecretInput) (*Secret, error) {

var body bytes.Buffer
err := json.NewEncoder(&body).Encode(struct {
Name string `json:"name"`
Secret []byte `json:"secret"`
Name string `json:"name"`
Secret []byte `json:"secret"`
ClientKey []byte `json:"client_key,omitempty"`
}{
Name: i.Name,
Secret: i.Secret,
Name: i.Name,
Secret: i.Secret,
ClientKey: i.ClientKey,
})
if err != nil {
return nil, err
Expand Down Expand Up @@ -379,3 +384,77 @@ func (c *Client) DeleteSecret(i *DeleteSecretInput) error {
}
return resp.Body.Close()
}

// ClientKey is an X25519 public key that can be used with
// golang.org/x/crypto/nacl/box to encrypt secrets locally before
// uploading them to the Fastly API. A client key is valid only for a
// short amount of time, and should be used immediately. The key is not
// valid after the ExpiresAt time.
//
// Client keys are signed, and the attached signature must be validated
// using the public signing key before it is used. A ValidateSignature
// method is provided for this purpose.
type ClientKey struct {
PublicKey []byte `json:"public_key"`
Signature []byte `json:"signature"`
ExpiresAt time.Time `json:"expires_at"`
}

// ValidateSignature returns true if the client key's signature for the
// provided public signing key.
func (ck *ClientKey) ValidateSignature(signingKey ed25519.PublicKey) bool {
return ed25519.Verify(signingKey, ck.PublicKey, ck.Signature)
}

// CreateClientKey creates a new time-limited client key for locally
// encrypting secrets before uploading them to the Fastly API.
func (c *Client) CreateClientKey() (*ClientKey, error) {
p := "/resources/stores/secret/client-key"

resp, err := c.Post(p, &RequestOptions{
Headers: map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
},
Parallel: true,
})
if err != nil {
return nil, err
}
defer resp.Body.Close()

var output ClientKey
if err := json.NewDecoder(resp.Body).Decode(&output); err != nil {
return nil, err
}

return &output, nil
}

// GetSigningKey returns the public signing key for client keys. In
// general the signing key changes very rarely, and it's recommended to
// ship the signing key out-of-band from the API.
func (c *Client) GetSigningKey() (ed25519.PublicKey, error) {
p := "/resources/stores/secret/signing-key"

resp, err := c.Get(p, &RequestOptions{
Headers: map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
},
Parallel: true,
})
if err != nil {
return nil, err
}
defer resp.Body.Close()

var output struct {
SigningKey []byte `json:"signing_key"`
}
if err := json.NewDecoder(resp.Body).Decode(&output); err != nil {
return nil, err
}

return ed25519.PublicKey(output.SigningKey), nil
}
Loading

0 comments on commit d50d58c

Please sign in to comment.