├── _config.yml
├── keymanager
├── fixtures
│ ├── keyid.txt
│ └── eckey.pem
├── mocks
│ └── testmockkeymanager.go
├── keymanager_test.go
└── keymanager.go
├── circle.yml
├── .gitignore
├── resources
└── 331px-Sputnik-stamp-ussr.jpg
├── requesthandling
├── fixtures
│ └── test_identity.der
├── payload.go
├── requestconfig_test.go
├── requestconfig.go
├── signing_test.go
├── requestmanager_test.go
└── requestmanager.go
├── sputnik.go
├── cmd
├── fixtures
│ ├── query_sample_payload.json
│ └── modify_sample_payload.json
├── commands_test.go
├── keyid.go
├── store.go
├── requests.go
├── print.go
├── create.go
├── identity.go
├── get.go
├── root.go
├── identitydelete.go
└── post.go
├── main.go
├── bitbucket-pipelines.yml
├── jenkinsfile
├── LICENSE
└── README.md
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-minimal
--------------------------------------------------------------------------------
/keymanager/fixtures/keyid.txt:
--------------------------------------------------------------------------------
1 | abc
2 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | test:
2 | override:
3 | - go test ./... -v
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | */.DS_Store
3 | sputnik
4 | go.mod
5 | go.sum
6 |
--------------------------------------------------------------------------------
/resources/331px-Sputnik-stamp-ussr.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/q231950/sputnik/HEAD/resources/331px-Sputnik-stamp-ussr.jpg
--------------------------------------------------------------------------------
/requesthandling/fixtures/test_identity.der:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/q231950/sputnik/HEAD/requesthandling/fixtures/test_identity.der
--------------------------------------------------------------------------------
/requesthandling/payload.go:
--------------------------------------------------------------------------------
1 | package requesthandling
2 |
3 | // Payload represents the payload to send with a request
4 | type Payload struct {
5 | Fields map[string]interface{}
6 | }
7 |
--------------------------------------------------------------------------------
/sputnik.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/apex/log"
4 |
5 | // Hello makes sure that the earth is still spinning around the sun
6 | func Hello() {
7 | log.Debug("This is спутник.")
8 | }
9 |
--------------------------------------------------------------------------------
/keymanager/fixtures/eckey.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN EC PRIVATE KEY-----
2 | MHcCAQEEIH7QO3OzlxJ8OqoVRgBBHJK/iQB1s22MiWLO5WNzNjoioAoGCCqGSM49
3 | AwEHoUQDQgAEuTDvRjrfQW7CHI68iJBpBpRkhT0JKMKA7vaSPvkckv9za3l1ji2L
4 | 0VsFSOIvSlgpUyC96pRxcIBR/E2gqmLbbA==
5 | -----END EC PRIVATE KEY-----
6 |
--------------------------------------------------------------------------------
/cmd/fixtures/query_sample_payload.json:
--------------------------------------------------------------------------------
1 | {
2 | "query":{
3 | "recordType":"City",
4 | "filterBy":[
5 | {
6 | "comparator":"EQUALS",
7 | "fieldName":"name",
8 | "fieldValue":{
9 | "value":"La Citta Nel Cielo"
10 | }
11 | }
12 | ]
13 | },
14 | "resultsLimit":1
15 | }
16 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/apex/log"
7 | "github.com/apex/log/handlers/cli"
8 |
9 | "github.com/q231950/sputnik/cmd"
10 | )
11 |
12 | func main() {
13 |
14 | log.SetHandler(cli.New(os.Stderr))
15 | log.SetLevel(log.InfoLevel)
16 |
17 | if err := cmd.RootCmd.Execute(); err != nil {
18 | log.Errorf("%s", err)
19 | os.Exit(-1)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/bitbucket-pipelines.yml:
--------------------------------------------------------------------------------
1 | image: golang:1.8
2 |
3 | pipelines:
4 | default:
5 | - step:
6 | script:
7 | - PACKAGE_PATH="${GOPATH}/src/bitbucket.org/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}"
8 | - mkdir -pv "${PACKAGE_PATH}"
9 | - tar -cO --exclude-vcs --exclude=bitbucket-pipelines.yml . | tar -xv -C "${PACKAGE_PATH}"
10 | - cd "${PACKAGE_PATH}"
11 | - go get -v -t -u
12 | - go build -v
13 | - go test ./... -v
14 |
--------------------------------------------------------------------------------
/requesthandling/requestconfig_test.go:
--------------------------------------------------------------------------------
1 | package requesthandling
2 |
3 | import "testing"
4 |
5 | func TestRequestConfigInit(t *testing.T) {
6 | config := NewRequestConfig("3", "com.test.go", "public")
7 | if config.Version != "3" {
8 | t.Errorf("Request Config Version has not been initialised correctly")
9 | }
10 |
11 | if config.ContainerID != "com.test.go" {
12 | t.Errorf("Request Container ID has not been initialised correctly")
13 | }
14 |
15 | if config.Database != "public" {
16 | t.Errorf("Request Database has not been initialised correctly")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/requesthandling/requestconfig.go:
--------------------------------------------------------------------------------
1 | package requesthandling
2 |
3 | // RequestConfig is used to initialise RequestManagers. It specifies the Cloudkit API version and container ID to use for requests
4 | type RequestConfig struct {
5 | Version string
6 | ContainerID string
7 | Database string
8 | }
9 |
10 | // NewRequestConfig creates a fresh config with the given version and container ID
11 | func NewRequestConfig(version string, containerID string, database string) RequestConfig {
12 | return RequestConfig{Version: version, ContainerID: containerID, Database: database}
13 | }
14 |
--------------------------------------------------------------------------------
/keymanager/mocks/testmockkeymanager.go:
--------------------------------------------------------------------------------
1 | package keymanager
2 |
3 | import (
4 | "crypto/ecdsa"
5 | "crypto/elliptic"
6 | "crypto/rand"
7 | )
8 |
9 | type MockKeyManager struct {
10 | }
11 |
12 | func (m MockKeyManager) PublicKey() *ecdsa.PublicKey {
13 | return nil
14 | }
15 |
16 | func (m MockKeyManager) PrivateKey() *ecdsa.PrivateKey {
17 | c := elliptic.P256()
18 | key, _ := ecdsa.GenerateKey(c, rand.Reader)
19 | return key
20 | }
21 |
22 | func (m MockKeyManager) KeyID() string {
23 | return "key id"
24 | }
25 |
26 | func (m MockKeyManager) RemoveSigningIdentity() error {
27 | return nil
28 | }
29 |
30 | func (m MockKeyManager) StoreKeyID(key string) error {
31 | return nil
32 | }
33 |
--------------------------------------------------------------------------------
/cmd/commands_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestRootCommand(t *testing.T) {
10 | flag := RootCmd.Flag("config")
11 | assert.NotNil(t, flag)
12 | }
13 |
14 | func TestRequestsCommand(t *testing.T) {
15 | run := requestsCmd.Run
16 | assert.NotNil(t, run)
17 | }
18 |
19 | func TestPostRequestCommand(t *testing.T) {
20 | run := postCmd.Run
21 | assert.NotNil(t, run)
22 | }
23 |
24 | func TestPostRequestCommandJSONFlag(t *testing.T) {
25 | flag := postCmd.Flag("json-file-path")
26 | assert.NotNil(t, flag)
27 | }
28 |
29 | func TestPostRequestCommandPayloadFlag(t *testing.T) {
30 | flag := postCmd.Flag("payload")
31 | assert.NotNil(t, flag)
32 | }
33 |
34 | func TestPostRequestCommandOperationFlag(t *testing.T) {
35 | flag := postCmd.Flag("operation")
36 | assert.NotNil(t, flag)
37 | }
38 |
--------------------------------------------------------------------------------
/jenkinsfile:
--------------------------------------------------------------------------------
1 | node {
2 | ws("${JENKINS_HOME}/jobs/${JOB_NAME}/builds/${BUILD_ID}/src/github.com/q231950/sputnik") {
3 | def root = tool name: 'Go 1.9.2', type: 'go'
4 | withEnv(["GOROOT=${root}", "GOPATH=${JENKINS_HOME}/jobs/${JOB_NAME}/builds/${BUILD_ID}/", "PATH+GO=${root}/bin"]) {
5 | env.PATH="${GOPATH}/bin:$PATH"
6 |
7 | stage("Clone") {
8 | checkout scm
9 | }
10 |
11 | stage("Get Dependencies") {
12 | sh 'go get -v'
13 | }
14 |
15 | stage("Get Test Dependencies") {
16 | sh "go get -u github.com/stretchr/testify/assert"
17 | sh "go get -u github.com/jstemmer/go-junit-report"
18 | }
19 |
20 | stage("Test") {
21 | sh 'go test ./... -v 2>&1 | go-junit-report > report.xml'
22 | }
23 |
24 | stage("Publish Results") {
25 | junit '**/report.xml'
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/cmd/fixtures/modify_sample_payload.json:
--------------------------------------------------------------------------------
1 | {
2 | "operations":[
3 | {
4 | "operationType":"create",
5 | "record":{
6 | "recordType":"City",
7 | "fields":{
8 | "geonameid":{
9 | "value":"my id"
10 | },
11 | "name":{
12 | "value":"La Citta Nel Cielo"
13 | },
14 | "alternatenames":{
15 | "value":""
16 | },
17 | "location":{
18 | "value":{
19 | "latitude":40.0,
20 | "longitude":10.0
21 | }
22 | },
23 | "countrycode":{
24 | "value":"AU"
25 | },
26 | "population":{
27 | "value":500
28 | },
29 | "elevation":{
30 | "value":800
31 | },
32 | "timezone":{
33 | "value":"some"
34 | }
35 | }
36 | }
37 | }
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Martin Kim Dung-Pham
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://circleci.com/bb/q231950/sputnik/tree/master) 
2 |
3 | # 🛰 спутник
4 |
5 | ## Talk to CloudKit. Server-to-Server in Go.
6 |
7 | > **Sputnik** enables you to connect to CloudKit from within your Go package or application using CloudKit's Server-to-Server Web Service API. **Sputnik** handles request signing for you and offers ways to interact with CloudKit directly from the CLI.
8 |
9 | ### Signing Requests
10 |
11 | Sputnik manages the most cumbersome part of CloudKit communication - the signing of your requests. For more information on signing have a look in the [Managing the Signing Identity](https://github.com/q231950/sputnik/wiki/Managing-the-Signing-Identity) section of the [Wiki](https://github.com/q231950/sputnik/wiki).
12 |
13 | ### Usage
14 |
15 | You can use Sputnik either from [the command line](https://github.com/q231950/sputnik/wiki/Sending-Requests#the-sputnik-binary) or [as a package](https://github.com/q231950/sputnik/wiki/Sending-Requests#the-sputnik-package). For more information about requests have a look in the [Sending Requests](https://github.com/q231950/sputnik/wiki/Sending-Requests) section of the [Wiki](https://github.com/q231950/sputnik/wiki).
16 |
17 | [Baikonur](https://github.com/q231950/baikonur) uses Sputnik to insert city records into a CloudKit container:
18 |
19 | ```go
20 | keyManager := keymanager.New()
21 |
22 | config := requesthandling.RequestConfig{Version: "1", ContainerID: "iCloud.com.some.bundle", Database: "public"}
23 | requestManager := requesthandling.New(config, &keyManager)
24 |
25 | request, error := requestManager.PostRequest("modify", json)
26 | client := &http.Client{}
27 | response, error := client.Do(request)
28 | ```
29 |
30 | ## State
31 |
32 | Please try this package and see how it works for you. Feedback and contributions are welcome <3
33 |
34 | 
35 |
--------------------------------------------------------------------------------
/cmd/keyid.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2017 Martin Kim Dung-Pham
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package cmd
22 |
23 | import (
24 | log "github.com/apex/log"
25 |
26 | "github.com/q231950/sputnik/keymanager"
27 | "github.com/spf13/cobra"
28 | )
29 |
30 | // keyidCmd represents the keyid command
31 | var keyidCmd = &cobra.Command{
32 | Use: "keyid",
33 | Short: "Show the key ID that is currently in use",
34 | Long: `You can provide a Cloudkit key ID by 2 methods
35 | #1 use the Sputnik command 'keyid store '
36 | #2 by setting an environment variable 'SPUTNIK_CLOUDKIT_KEYID'`,
37 | Run: func(cmd *cobra.Command, args []string) {
38 | log.Info("Attempting to retrieve the current key id...")
39 | keyManager := keymanager.New()
40 | keyID := keyManager.KeyID()
41 | if len(keyID) > 0 {
42 | log.Infof("The following key id is stored: `%s`", keyID)
43 | } else {
44 | log.Error("No iCloud key id specified. Please either provide one by `sputnik keyid store ` or set the environment variable `SPUTNIK_CLOUDKIT_KEYID`.")
45 | }
46 | },
47 | }
48 |
49 | func init() {
50 | RootCmd.AddCommand(keyidCmd)
51 | }
52 |
--------------------------------------------------------------------------------
/cmd/store.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2017 Martin Kim Dung-Pham
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package cmd
22 |
23 | import (
24 | log "github.com/apex/log"
25 | "github.com/q231950/sputnik/keymanager"
26 | "github.com/spf13/cobra"
27 | )
28 |
29 | // storeCmd represents the store command
30 | var storeCmd = &cobra.Command{
31 | Use: "store",
32 | Short: "Stores the Key ID from the Cloudkit Dashboard.",
33 | Long: `After providing the Cloudkit Dashboard with the public key, the keyId is generated.
34 | Use this command to store the key ID in Sputnik's secrets folder in ~/.sputnik/secrets/`,
35 | Run: func(cmd *cobra.Command, args []string) {
36 | if len(args) == 1 {
37 | keyID := args[0]
38 | keyManager := keymanager.New()
39 | err := keyManager.StoreKeyID(keyID)
40 | if err != nil {
41 | log.Errorf("Failed to store kei id (%s)", err)
42 | } else {
43 | log.Infof("The following key id has been stored: `%s`", keyID)
44 | }
45 | } else {
46 | log.Error("`keyid store` requires one argument which is the key id to store.")
47 | }
48 | },
49 | }
50 |
51 | func init() {
52 | keyidCmd.AddCommand(storeCmd)
53 | }
54 |
--------------------------------------------------------------------------------
/cmd/requests.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2017 Martin Kim Dung-Pham
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package cmd
22 |
23 | import (
24 | "fmt"
25 | "io/ioutil"
26 |
27 | "github.com/apex/log"
28 | "github.com/spf13/cobra"
29 | )
30 |
31 | var payloadFilePath string
32 | var payload string
33 | var operation string
34 | var container string
35 |
36 | // requestsCmd represents the requests command
37 | var requestsCmd = &cobra.Command{
38 | Use: "requests",
39 | Short: "A brief description of your command",
40 | Long: `A longer description that spans multiple lines and likely contains examples
41 | and usage of using your command. For example:
42 |
43 | Cobra is a CLI library for Go that empowers applications.
44 | This application is a tool to generate the needed files
45 | to quickly create a Cobra application.`,
46 | Run: func(cmd *cobra.Command, args []string) {
47 | fmt.Println("requests called")
48 | },
49 | }
50 |
51 | func init() {
52 | RootCmd.AddCommand(requestsCmd)
53 | }
54 |
55 | func payloadFromFile(path string) string {
56 | bytes, err := ioutil.ReadFile(path)
57 | if err != nil {
58 | log.Error(err.Error())
59 | }
60 | return string(bytes)
61 | }
62 |
--------------------------------------------------------------------------------
/requesthandling/signing_test.go:
--------------------------------------------------------------------------------
1 | package requesthandling
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | keymanager "github.com/q231950/sputnik/keymanager/mocks"
8 | )
9 |
10 | type TestableRequestManager interface {
11 | RequestManager
12 | HashedBody(body string) string
13 | }
14 |
15 | func TestPostRequestDateParameterIsInPerimeter(t *testing.T) {
16 | keyManager := keymanager.MockKeyManager{}
17 | config := RequestConfig{Version: "1", ContainerID: "containerid", Database: "public"}
18 | requestManager := New(config, &keyManager)
19 | request, _ := requestManager.PostRequest("users/caller", "")
20 | dateString := request.Header.Get("X-Apple-CloudKit-Request-ISO8601Date")
21 |
22 | expectedTime := time.Now().UTC()
23 | roundedExpectedTime := expectedTime.Round(time.Minute)
24 |
25 | actualTime, _ := time.Parse(time.RFC3339, dateString)
26 | roundedTime := actualTime.Round(time.Minute)
27 |
28 | if !roundedExpectedTime.Equal(roundedTime) {
29 | t.Errorf("The date parameter must not differ by more than a minute from now")
30 | }
31 | }
32 |
33 | func TestMessageFormat(t *testing.T) {
34 | keyManager := keymanager.MockKeyManager{}
35 | config := RequestConfig{Version: "1", ContainerID: "containerid", Database: "public"}
36 | requestManager := New(config, &keyManager)
37 | message := requestManager.message("date", "body", "service url")
38 | if message != "date:body:service url" {
39 | t.Errorf("The request payload needs to be properly formatted")
40 | }
41 | }
42 |
43 | func TestEmptyHashedBody(t *testing.T) {
44 | keyManager := keymanager.MockKeyManager{}
45 | config := NewRequestConfig("version", "containerID", "public")
46 | requestManager := TestableRequestManager(&CloudkitRequestManager{config, &keyManager})
47 | body := ""
48 | hash := requestManager.HashedBody(body)
49 |
50 | if string(hash) != "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" {
51 | t.Errorf("Signature is not correct")
52 | }
53 | }
54 |
55 | func TestSignMessage(t *testing.T) {
56 | keyManager := keymanager.MockKeyManager{}
57 | config := NewRequestConfig("version", "containerID", "public")
58 | r := New(config, &keyManager)
59 | signature := r.SignatureForMessage([]byte("message"))
60 |
61 | if signature == nil {
62 | t.Errorf("A message should be signed when a private key is available")
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/cmd/print.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Martin Kim Dung-Pham
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package cmd
22 |
23 | import (
24 | log "github.com/apex/log"
25 |
26 | "github.com/q231950/sputnik/keymanager"
27 | "github.com/spf13/cobra"
28 | )
29 |
30 | // printCmd represents the print command
31 | var printCmd = &cobra.Command{
32 | Use: "print",
33 | Short: "Print the public key",
34 | Long: `Use this command to get the public key to paste into the CloudKit Dashboard when granting API access.`,
35 | Run: func(cmd *cobra.Command, args []string) {
36 | log.Info("Attempting to print the public key")
37 |
38 | keyManager := keymanager.New()
39 | exists, err := keyManager.SigningIdentityExists()
40 | if err != nil {
41 | log.Errorf("Error in SigningIdentityExists: %s", err)
42 | }
43 | if exists {
44 | log.Infof("Printing the public key\n%s", keyManager.PublicKeyString())
45 | } else {
46 | log.Info("The ec key does not exist, need to create, one moment, please")
47 | keyManager.CreateSigningIdentity()
48 |
49 | log.Infof("Ok done. This is it: \n%s", keyManager.PublicKeyString())
50 | }
51 | },
52 | }
53 |
54 | func init() {
55 | eckeyCmd.AddCommand(printCmd)
56 | }
57 |
--------------------------------------------------------------------------------
/cmd/create.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Martin Kim Dung-Pham
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package cmd
22 |
23 | import (
24 | log "github.com/apex/log"
25 |
26 | "github.com/q231950/sputnik/keymanager"
27 | "github.com/spf13/cobra"
28 | )
29 |
30 | // createCmd represents the create command
31 | var createCmd = &cobra.Command{
32 | Use: "create",
33 | Short: "Creates a new signing identity",
34 | Long: `For now, a file named eckey.pem will be put into the secrets folder.`,
35 | Run: func(cmd *cobra.Command, args []string) {
36 | log.Infof("Attempting to create a new identity...")
37 | createECKey()
38 | },
39 | }
40 |
41 | func init() {
42 | eckeyCmd.AddCommand(createCmd)
43 | }
44 |
45 | func createECKey() {
46 | keyManager := keymanager.New()
47 | exists, err := keyManager.SigningIdentityExists()
48 | if err != nil {
49 | log.Errorf("Error in SigningIdentityExists: %s", err)
50 | }
51 |
52 | if exists {
53 | log.Error("There is an existing identity. You need to remove it with `./sputnik identity remove` before you can create a new one.")
54 | if len(keyManager.KeyID()) != 0 {
55 | // a key ID has been stored
56 | log.Infof("The current identity is linked with the following iCloud key ID:\n%s", keyManager.KeyID())
57 | } else {
58 | log.Infof("This is the current identity:\n%s", keyManager.ECKey())
59 | }
60 | } else {
61 | log.Info("Creating an identity")
62 | keyManager.CreateSigningIdentity()
63 | log.Info("Done")
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/cmd/identity.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Martin Kim Dung-Pham
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package cmd
22 |
23 | import (
24 | log "github.com/apex/log"
25 |
26 | "github.com/q231950/sputnik/keymanager"
27 | "github.com/spf13/cobra"
28 | )
29 |
30 | // The identity command shows the current identity.
31 | var eckeyCmd = &cobra.Command{
32 | Use: "identity",
33 | Short: "Show the signing identity",
34 | Long: `Show the signing identity that is used for signing the iCloud requests.`,
35 | Run: func(cmd *cobra.Command, args []string) {
36 | log.Debug("Attempting to retrieve the current identity...")
37 | keyManager := keymanager.New()
38 | keyExists, err := keyManager.SigningIdentityExists()
39 | if err != nil {
40 | log.Errorf("Error in SigningIdentityExists: %s", err)
41 | }
42 |
43 | if keyExists {
44 | identity := keyManager.ECKey()
45 | log.Debug("The current identity you can create a new server-to-server key with in the iCloud Dashboard:")
46 | log.Infof("\n%s", identity)
47 |
48 | keyID := keyManager.KeyID()
49 | if len(keyID) == 0 {
50 | log.Error("No iCloud KeyID specified. Please either provide one by `sputnik keyid store ` or set the environment variable `SPUTNIK_CLOUDKIT_KEYID`.")
51 | }
52 | } else {
53 | log.Error("A signing identity could not be found. A signing identity can be created by `./sputnik identity create`")
54 | }
55 | },
56 | }
57 |
58 | func init() {
59 | RootCmd.AddCommand(eckeyCmd)
60 | }
61 |
--------------------------------------------------------------------------------
/cmd/get.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2017 Martin Kim Dung-Pham
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package cmd
22 |
23 | import (
24 | "github.com/apex/log"
25 | "github.com/q231950/sputnik/keymanager"
26 | "github.com/q231950/sputnik/requesthandling"
27 | "github.com/spf13/cobra"
28 | )
29 |
30 | // getCmd represents the get command
31 | var getCmd = &cobra.Command{
32 | Use: "get",
33 | Short: "A brief description of your command",
34 | Long: `A longer description that spans multiple lines and likely contains examples
35 | and usage of using your command. For example:
36 |
37 | Cobra is a CLI library for Go that empowers applications.
38 | This application is a tool to generate the needed files
39 | to quickly create a Cobra application.`,
40 | Run: func(cmd *cobra.Command, args []string) {
41 | log.WithField("Payload", payload).Info("Attempting to GET...")
42 | keyManager := keymanager.New()
43 | config := requesthandling.RequestConfig{}
44 | requestManager := requesthandling.New(config, &keyManager)
45 | requestManager.GetRequest("lookup", "{}")
46 | },
47 | }
48 |
49 | func init() {
50 | requestsCmd.AddCommand(getCmd)
51 |
52 | getCmd.Flags().StringVarP(&payloadFilePath, "json-file-path", "j", "", "A path to a file that contains the json payload")
53 | getCmd.Flags().StringVarP(&payload, "payload", "p", "", "A json payload as string")
54 | getCmd.Flags().StringVarP(&operation, "operation", "o", "", "The operation to execute: Depending on your intention, operation-specific subpaths may be of [modify, query, lookup, changes, resolve, accept]")
55 | }
56 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Martin Kim Dung-Pham
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package cmd
22 |
23 | import (
24 | "os"
25 |
26 | log "github.com/apex/log"
27 |
28 | "github.com/spf13/cobra"
29 | "github.com/spf13/viper"
30 | )
31 |
32 | var cfgFile string
33 |
34 | // RootCmd represents the base command when called without any subcommands
35 | var RootCmd = &cobra.Command{
36 | Use: "sputnik",
37 | Short: "sputnik talks to CloudKit",
38 | Long: `спутник talks to CloudKit:
39 |
40 | Easily communicate server to server using CloudKit in the app and Go in your backend.️`,
41 | }
42 |
43 | // Execute adds all child commands to the root command sets flags appropriately.
44 | // This is called by main.main(). It only needs to happen once to the rootCmd.
45 | func Execute() {
46 | if err := RootCmd.Execute(); err != nil {
47 | log.Errorf("%s", err)
48 | os.Exit(-1)
49 | }
50 | }
51 |
52 | func init() {
53 | cobra.OnInitialize(initConfig)
54 |
55 | RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.sputnik.yaml)")
56 | }
57 |
58 | // initConfig reads in config file and ENV variables if set.
59 | func initConfig() {
60 | if cfgFile != "" { // enable ability to specify config file via flag
61 | viper.SetConfigFile(cfgFile)
62 | }
63 |
64 | viper.SetConfigName(".sputnik") // name of config file (without extension)
65 | viper.AddConfigPath("$HOME") // adding home directory as first search path
66 | viper.AutomaticEnv() // read in environment variables that match
67 |
68 | // If a config file is found, read it in.
69 | if err := viper.ReadInConfig(); err == nil {
70 | log.Debugf("Using config file: `%s`", viper.ConfigFileUsed())
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/cmd/identitydelete.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2017 Martin Kim Dung-Pham
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package cmd
22 |
23 | import (
24 | log "github.com/apex/log"
25 | "github.com/q231950/sputnik/keymanager"
26 | "github.com/spf13/cobra"
27 | )
28 |
29 | // identitydeleteCmd represents the identitydelete command
30 | var identitydeleteCmd = &cobra.Command{
31 | Use: "remove",
32 | Short: "Removes the signing identity",
33 | Long: `
34 | This command is destructive!
35 |
36 | 'remove' removes the current signing identity. This makes the key ID in the Cloudkit Dashboard useless. After running this command you should also revoke the key ID in the matching container in your https://icloud.developer.apple.com/dashboard/.`,
37 | Run: func(cmd *cobra.Command, args []string) {
38 | log.Info("Attempting to remove the current identity...")
39 | keyManager := keymanager.New()
40 | exists, err := keyManager.SigningIdentityExists()
41 | if err != nil {
42 | log.Errorf("Error in SigningIdentityExists: %s", err)
43 | }
44 | if exists {
45 | removeSigningIdentity(&keyManager)
46 | } else {
47 | log.Warn("There is no signing identity to remove.")
48 | }
49 | },
50 | }
51 |
52 | func removeSigningIdentity(keyManager keymanager.KeyManager) {
53 | pub := keyManager.PublicKey()
54 | keyID := keyManager.KeyID()
55 | err := keyManager.RemoveSigningIdentity()
56 | if err != nil {
57 | log.Errorf("An error occurred while removing the signing identity (%s)", err)
58 | } else {
59 | log.Info("Your signing identity has been removed. Make sure to revoke the corresponding KeyID in the Cloudkit Dashboard.")
60 | log.Infof("The identity with the following public key was removed:\n%s", pub)
61 | log.Infof("The following key ID is now useless (unless you kept a copy of the private key somewhere outside of Sputnik):\n%s", keyID)
62 | }
63 | }
64 |
65 | func init() {
66 | eckeyCmd.AddCommand(identitydeleteCmd)
67 | }
68 |
--------------------------------------------------------------------------------
/requesthandling/requestmanager_test.go:
--------------------------------------------------------------------------------
1 | package requesthandling
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "testing"
7 | "time"
8 |
9 | mocks "github.com/q231950/sputnik/keymanager/mocks"
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | // This Example shows how to create a request manager.
14 | //
15 | // A request manager requires a keymanager for handling authentication as well as a valid configuration.
16 | func ExampleRequestManager() {
17 | keyManager := mocks.MockKeyManager{}
18 | config := RequestConfig{Version: "1", ContainerID: "iCloud.com.mycontainer", Database: "public"}
19 | requestManager := New(config, &keyManager)
20 | fmt.Printf("Container:%s, Version:%s, Database:%s",
21 | requestManager.Config.ContainerID,
22 | requestManager.Config.Version,
23 | requestManager.Config.Database)
24 | // Output: Container:iCloud.com.mycontainer, Version:1, Database:public
25 | }
26 |
27 | func TestNewRequestManager(t *testing.T) {
28 | keyManager := mocks.MockKeyManager{}
29 | config := RequestConfig{Version: "1", ContainerID: "iCloud.com.elbedev.shelve.dev", Database: "public"}
30 | requestManager := New(config, keyManager)
31 | if requestManager.keyManager != keyManager {
32 | t.Errorf("A Request Manager's key manager should be the same that was used at initialisation")
33 | }
34 | }
35 |
36 | func TestPostRequest(t *testing.T) {
37 | keyManager := mocks.MockKeyManager{}
38 | config := RequestConfig{Version: "1", ContainerID: "iCloud.com.elbedev.shelve.dev", Database: "public"}
39 | requestManager := New(config, &keyManager)
40 | request, _ := requestManager.PostRequest("modify", "")
41 |
42 | assert.Equal(t, "/database/1/iCloud.com.elbedev.shelve.dev/development/public/records/modify", request.URL.Path)
43 | }
44 |
45 | func samplePostRequest() (*http.Request, error) {
46 | keyManager := mocks.MockKeyManager{}
47 | config := RequestConfig{Version: "1", ContainerID: "iCloud.com.elbedev.shelve.dev", Database: "public"}
48 | requestManager := New(config, &keyManager)
49 | return requestManager.request("some_operation", POST, `{"key":"value", "keys":["value1", "value2"]}`)
50 | }
51 |
52 | func TestRequest(t *testing.T) {
53 | request, err := samplePostRequest()
54 | assert.NotNil(t, request)
55 | assert.Nil(t, err)
56 | }
57 |
58 | func TestRequestMethod(t *testing.T) {
59 | request, _ := samplePostRequest()
60 | assert.Equal(t, "POST", request.Method)
61 | }
62 |
63 | func TestRequestDate(t *testing.T) {
64 | request, _ := samplePostRequest()
65 | dateString := request.Header.Get("X-Apple-CloudKit-Request-ISO8601Date")
66 |
67 | expectedTime := time.Now().UTC()
68 | roundedExpectedTime := expectedTime.Round(time.Minute)
69 | actualTime, _ := time.Parse(time.RFC3339, dateString)
70 | roundedTime := actualTime.Round(time.Minute)
71 |
72 | assert.Equal(t, roundedExpectedTime, roundedTime, "The date parameter must not differ by more than a minute from now")
73 | }
74 |
75 | func TestPayloadFormat(t *testing.T) {
76 | keyManager := mocks.MockKeyManager{}
77 | config := RequestConfig{Version: "1", ContainerID: "iCloud.com.elbedev.shelve.dev", Database: "public"}
78 | requestManager := New(config, &keyManager)
79 | message := requestManager.message("date", "body", "service url")
80 | if message != "date:body:service url" {
81 | t.Errorf("The request payload needs to be properly formatted")
82 | }
83 | }
84 |
85 | func TestRequestHost(t *testing.T) {
86 | request, _ := samplePostRequest()
87 | assert.Equal(t, "api.apple-cloudkit.com", request.URL.Host)
88 | }
89 |
90 | func TestRequestScheme(t *testing.T) {
91 | request, _ := samplePostRequest()
92 | assert.Equal(t, "https", request.URL.Scheme)
93 | }
94 |
--------------------------------------------------------------------------------
/cmd/post.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2017 Martin Kim Dung-Pham
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package cmd
22 |
23 | import (
24 | "encoding/json"
25 | "io/ioutil"
26 | "net/http"
27 |
28 | log "github.com/apex/log"
29 | "github.com/q231950/sputnik/keymanager"
30 | "github.com/q231950/sputnik/requesthandling"
31 | "github.com/spf13/cobra"
32 | )
33 |
34 | // postCmd represents the post command
35 | var postCmd = &cobra.Command{
36 | Use: "post",
37 | Short: "post allows you to send post requests with a given payload",
38 | Long: `post allows you to send post requests with a given payload:
39 |
40 | Here, a payload from file is post'ed with the operation modify
41 | ./sputnik requests post --operation "modify" --payload-file-path 'path/to/file.json'
42 | ./sputnik requests post -o "modify" --pf 'path/to/file.json'
43 |
44 | ./sputnik requests post --operation "modify" --payload ''
45 | ./sputnik requests post -o "modify" -p ''
46 |
47 | `,
48 | Run: func(cmd *cobra.Command, args []string) {
49 | log.WithFields(log.Fields{
50 | "Operation": operation}).Info("Attempting to POST...")
51 |
52 | if operation == "" {
53 | log.Error("Missing operation, please provide one. See `sputnik help requests post`")
54 | }
55 |
56 | var payloadToUse string
57 | if payloadFilePath != "" {
58 | payloadToUse = payloadFromFile(payloadFilePath)
59 | log.WithFields(log.Fields{
60 | "Payload": payloadToUse}).Info("Payload from file")
61 |
62 | } else {
63 | payloadToUse = payload
64 | }
65 |
66 | keyManager := keymanager.New()
67 |
68 | if container != "" {
69 | config := requesthandling.RequestConfig{Version: "1", Database: "public", ContainerID: container}
70 | requestManager := requesthandling.New(config, &keyManager)
71 |
72 | request, err := requestManager.PostRequest(operation, payloadToUse)
73 | if err != nil {
74 | log.Error(err.Error())
75 | } else {
76 | client := http.Client{}
77 | resp, err := client.Do(request)
78 | if err != nil {
79 | panic(err)
80 | } else {
81 | body, _ := ioutil.ReadAll(resp.Body)
82 | s, _ := json.MarshalIndent(string(body), "", " ")
83 | log.Info(string(s))
84 | }
85 | }
86 | } else {
87 | log.Error("Missing container, please provide one. See `sputnik help requests post`")
88 | }
89 | },
90 | }
91 |
92 | func init() {
93 | requestsCmd.AddCommand(postCmd)
94 | postCmd.Flags().StringVarP(&payloadFilePath, "json-file-path", "j", "", "A path to a file that contains the json payload")
95 | postCmd.Flags().StringVarP(&payload, "payload", "p", "", "A json payload as string")
96 | postCmd.Flags().StringVarP(&operation, "operation", "o", "", "The operation to execute: Depending on your intention, operation-specific subpaths may be of [modify, query, lookup, changes, resolve, accept]")
97 | postCmd.Flags().StringVarP(&container, "container", "c", "", "The CloudKit container to access. (normally `iCloud.your.bundle.identifier`)")
98 | }
99 |
--------------------------------------------------------------------------------
/requesthandling/requestmanager.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package requesthandling offers means to create signed requests for interfacing with CloudKit.
3 |
4 | A Configuration is required to know the CloudKit container's properties, the KeyManager is required for signing requests.
5 | */
6 | package requesthandling
7 |
8 | import (
9 | "bytes"
10 | "crypto"
11 | "crypto/rand"
12 | "crypto/sha256"
13 | "encoding/base64"
14 | "net/http"
15 | "strings"
16 | "time"
17 |
18 | log "github.com/apex/log"
19 | "github.com/q231950/sputnik/keymanager"
20 | )
21 |
22 | // The HTTPMethod defines the method of a request
23 | type HTTPMethod string
24 |
25 | const (
26 | // GET represents HTTP GET
27 | GET HTTPMethod = "GET"
28 | // POST represents HTTP POST
29 | POST = "POST"
30 | // PUT represents HTTP PUT
31 | PUT = "PUT"
32 | )
33 |
34 | // The RequestManager interface exposes methods for creating requests
35 | type RequestManager interface {
36 | PostRequest(string, string) (*http.Request, error)
37 | GetRequest(string, string) (*http.Request, error)
38 | }
39 |
40 | // CloudkitRequestManager is the concrete implementation of RequestManager
41 | type CloudkitRequestManager struct {
42 | Config RequestConfig
43 | keyManager keymanager.KeyManager
44 | }
45 |
46 | // New creates a new RequestManager
47 | func New(config RequestConfig, keyManager keymanager.KeyManager) CloudkitRequestManager {
48 | return CloudkitRequestManager{Config: config, keyManager: keyManager}
49 | }
50 |
51 | // PostRequest is a convenience method for creating POST requests
52 | func (cm CloudkitRequestManager) PostRequest(operationPath string, body string) (*http.Request, error) {
53 | return cm.request(operationPath, POST, body)
54 | }
55 |
56 | // GetRequest is a convenience method for creating POST requests
57 | func (cm CloudkitRequestManager) GetRequest(operationPath string, body string) (*http.Request, error) {
58 | return cm.request(operationPath, GET, body)
59 | }
60 |
61 | // Request creates a signed request with the given parameters
62 | func (cm *CloudkitRequestManager) request(p string, method HTTPMethod, payload string) (*http.Request, error) {
63 | keyID := cm.keyManager.KeyID()
64 |
65 | currentDate := cm.formattedTime(time.Now())
66 | path := cm.subpath(p)
67 | hashedBody := cm.HashedBody(payload)
68 | message := cm.message(currentDate, hashedBody, path)
69 | signature := cm.SignatureForMessage([]byte(message))
70 | encodedSignature := string(base64.StdEncoding.EncodeToString(signature))
71 | url := "https://api.apple-cloudkit.com" + path
72 |
73 | log.WithFields(log.Fields{
74 | "key id": keyID,
75 | "date": currentDate,
76 | "body": hashedBody,
77 | "base64 encoded signature": encodedSignature,
78 | "path": path}).Debug("Creating request")
79 |
80 | return cm.requestWithHeaders(string(method), url, []byte(payload), keyID, currentDate, encodedSignature)
81 | }
82 |
83 | // request creates a request with the given parameters.
84 | // - method POST/GET/...
85 | // - body is used as body for POST requests.
86 | // - url the request's endpoint
87 | // - keyID Header parameter X-Apple-CloudKit-Request-KeyID
88 | // - date Header parameter X-Apple-CloudKit-Request-ISO8601Date
89 | // - signature Header parameter X-Apple-CloudKit-Request-SignatureV1
90 | func (cm *CloudkitRequestManager) requestWithHeaders(method string, url string, body []byte, keyID string, date string, signature string) (request *http.Request, err error) {
91 | request, err = http.NewRequest(method, url, bytes.NewBuffer(body))
92 | request.Header.Set("X-Apple-CloudKit-Request-KeyID", keyID)
93 | request.Header.Set("X-Apple-CloudKit-Request-ISO8601Date", date)
94 | request.Header.Set("X-Apple-CloudKit-Request-SignatureV1", signature)
95 | log.WithField("url", request.URL).Debug("Added headers to request")
96 | return request, err
97 | }
98 |
99 | // SignatureForMessage returns the signature for the given message
100 | func (cm *CloudkitRequestManager) SignatureForMessage(message []byte) (signature []byte) {
101 | priv := cm.keyManager.PrivateKey()
102 | rand := rand.Reader
103 |
104 | h := sha256.New()
105 | h.Write([]byte(message))
106 |
107 | opts := crypto.SHA256
108 | if priv != nil {
109 | signature, err := priv.Sign(rand, h.Sum(nil), opts)
110 | if err != nil {
111 | log.WithError(err).Error("Unable to sign message")
112 | }
113 |
114 | return signature
115 | }
116 |
117 | log.Fatal("Can't sign without a private key")
118 |
119 | return nil
120 | }
121 |
122 | func (cm *CloudkitRequestManager) subpath(path string) string {
123 | version := cm.Config.Version
124 | containerID := cm.Config.ContainerID
125 | components := []string{"/database", version, containerID, "development", cm.Config.Database, "records", path}
126 | return strings.Join(components, "/")
127 | }
128 |
129 | func (cm *CloudkitRequestManager) formattedTime(t time.Time) string {
130 | date := t.UTC().Format(time.RFC3339)
131 | return date
132 | }
133 |
134 | func (cm *CloudkitRequestManager) message(date string, payload string, path string) string {
135 | components := []string{date, payload, path}
136 | message := strings.Join(components, ":")
137 | return message
138 | }
139 |
140 | // HashedBody takes the given body, hashes it, using sha256 and returns the base64 encoded result
141 | func (cm *CloudkitRequestManager) HashedBody(body string) string {
142 | h := sha256.New()
143 | h.Write([]byte(body))
144 | return base64.StdEncoding.EncodeToString([]byte(h.Sum(nil)))
145 | }
146 |
--------------------------------------------------------------------------------
/keymanager/keymanager_test.go:
--------------------------------------------------------------------------------
1 | package keymanager
2 |
3 | import (
4 | "math/big"
5 | "os"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestNew(t *testing.T) {
12 | manager := New()
13 | assert.NotNil(t, manager, "A newly created key manager should not be nil")
14 | }
15 |
16 | func TestStoredKeyID(t *testing.T) {
17 | pathToFixtures := "./fixtures"
18 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem")
19 | assert.Equal(t, "abc\n", manager.KeyID(), "A stored key id should get found")
20 | }
21 |
22 | func TestPrivateKey(t *testing.T) {
23 | pathToFixtures := "./fixtures"
24 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem")
25 |
26 | expectedD := new(big.Int)
27 | expectedD.SetString("57359333433306843951573675484597381433848383364258304847182053853963006392866", 10)
28 | assert.Equal(t, expectedD, manager.PrivateKey().D, "The private key does not match the certificate")
29 | }
30 |
31 | func TestPrivateKeyFromMemory(t *testing.T) {
32 | pathToFixtures := "./fixtures"
33 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem")
34 |
35 | privateKey := manager.PrivateKey()
36 | privateKey = manager.PrivateKey()
37 | expectedD := new(big.Int)
38 | expectedD.SetString("57359333433306843951573675484597381433848383364258304847182053853963006392866", 10)
39 | assert.Equal(t, expectedD, privateKey.D, "The private key does not match the certificate")
40 | }
41 |
42 | func TestPublicKey(t *testing.T) {
43 | pathToFixtures := "./fixtures"
44 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem")
45 |
46 | publicKey := manager.PublicKey()
47 | expectedX := new(big.Int)
48 | expectedX.SetString("83764337057786748884235593670888306280068598385338703097790983192099570881279", 10)
49 | assert.Equal(t, expectedX, publicKey.X, "The public key's X does not match the certificate")
50 |
51 | expectedY := new(big.Int)
52 | expectedY.SetString("52205868503601725522780045291819081986668283346034058232083577220213552372588", 10)
53 | assert.Equal(t, expectedY, publicKey.Y, "The public key's Y does not match the certificate")
54 | }
55 |
56 | func TestPublicKeyFromMemory(t *testing.T) {
57 | pathToFixtures := "./fixtures"
58 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem")
59 |
60 | publicKey := manager.PublicKey()
61 | publicKey = manager.PublicKey()
62 | expectedX := new(big.Int)
63 | expectedX.SetString("83764337057786748884235593670888306280068598385338703097790983192099570881279", 10)
64 | assert.Equal(t, expectedX, publicKey.X, "The public key's X does not match the certificate")
65 | }
66 |
67 | func TestStoreKeyID(t *testing.T) {
68 | pathToFixtures := "./testFiles"
69 | manager := NewWithSecretsFolder(pathToFixtures, "keyidCreateTest.txt", "eckeyTest.pem")
70 |
71 | manager.StoreKeyID("key")
72 | assert.Equal(t, "key", manager.KeyID(), "The key id should be stored correctly")
73 |
74 | _ = os.RemoveAll("./testFiles")
75 | }
76 |
77 | func TestSigningIdentityExists(t *testing.T) {
78 | pathToFixtures := "./fixtures"
79 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem")
80 | exists, _ := manager.SigningIdentityExists()
81 | assert.True(t, exists, "The signing identity should exist in this fixture")
82 | }
83 |
84 | func TestECKey(t *testing.T) {
85 | pathToFixtures := "./fixtures"
86 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem")
87 | expected := "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuTDvRjrfQW7CHI68iJBpBpRkhT0J\nKMKA7vaSPvkckv9za3l1ji2L0VsFSOIvSlgpUyC96pRxcIBR/E2gqmLbbA==\n-----END PUBLIC KEY-----\n"
88 | assert.Equal(t, expected, manager.ECKey(), "The public key is not correct")
89 | }
90 |
91 | func TestCreateIdentity(t *testing.T) {
92 | pathToFixtures := "./testFiles"
93 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem")
94 | err := manager.CreateSigningIdentity()
95 | assert.Nil(t, err, "It should be possible to create a signing identity")
96 |
97 | _ = os.RemoveAll("./testFiles")
98 | }
99 |
100 | func TestCreateIdentityPEMError(t *testing.T) {
101 | pathToFixtures := "./testFiles"
102 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "")
103 | err := manager.CreateSigningIdentity()
104 | assert.NotNil(t, err, "An error should occur when creating the signing identity fails due to a false pem file name")
105 |
106 | _ = os.RemoveAll("./testFiles")
107 | }
108 |
109 | func TestRemovesECKeyKeyWhenRemovingSigningIdentity(t *testing.T) {
110 | pathToFixtures := "./testFiles"
111 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem")
112 | _ = manager.CreateSigningIdentity()
113 |
114 | manager.RemoveSigningIdentity()
115 |
116 | assert.Equal(t, "", manager.ECKey(), "The public key should be gone after deleting the signing identity") // TODO get this right
117 |
118 | _ = os.RemoveAll("./testFiles")
119 | }
120 |
121 | func TestRemovesPublicKeyWhenRemovingSigningIdentity(t *testing.T) {
122 | pathToFixtures := "./testFiles"
123 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem")
124 | _ = manager.CreateSigningIdentity()
125 |
126 | manager.RemoveSigningIdentity()
127 |
128 | assert.Nil(t, manager.PublicKey(), "The public key should be gone after deleting the signing identity")
129 |
130 | _ = os.RemoveAll("./testFiles")
131 | }
132 |
133 | func TestRemovesPrivateKeyWhenRemovngSigningIdentity(t *testing.T) {
134 | pathToFixtures := "./testFiles"
135 | manager := NewWithSecretsFolder(pathToFixtures, "keyid.txt", "eckey.pem")
136 | _ = manager.CreateSigningIdentity()
137 |
138 | manager.RemoveSigningIdentity()
139 |
140 | assert.Nil(t, manager.PrivateKey(), "The private key should be gone after deleting the signing identity")
141 |
142 | _ = os.RemoveAll("./testFiles")
143 | }
144 |
--------------------------------------------------------------------------------
/keymanager/keymanager.go:
--------------------------------------------------------------------------------
1 | package keymanager
2 |
3 | import (
4 | "crypto/ecdsa"
5 | "crypto/x509"
6 | "encoding/pem"
7 | "errors"
8 | "io/ioutil"
9 | "os"
10 | "os/exec"
11 | "os/user"
12 | "strings"
13 |
14 | log "github.com/apex/log"
15 | )
16 |
17 | // KeyIDEnvironmentVariableName is the constant used for identifying the Key ID environment variable
18 | const KeyIDEnvironmentVariableName = string("SPUTNIK_CLOUDKIT_KEYID")
19 |
20 | // KeyManager exposes methods for creating, reading and removing signing identity relevant keys and IDs
21 | type KeyManager interface {
22 | PublicKey() *ecdsa.PublicKey
23 | PrivateKey() *ecdsa.PrivateKey
24 | KeyID() string
25 | RemoveSigningIdentity() error
26 | StoreKeyID(key string) error
27 | }
28 |
29 | // CloudKitKeyManager is a concrete KeyManager
30 | type CloudKitKeyManager struct {
31 | secretsFolder string
32 | pemFileName string
33 | keyIDFileName string
34 | inMemoryKeyID string
35 | inMemoryPrivateKey *ecdsa.PrivateKey
36 | inMemoryPublicKey *ecdsa.PublicKey
37 | }
38 |
39 | // New returns a CloudKitKeyManager with the default secrets folder
40 | // By default, a CloudKitKeyManager expects the secrets in the .sputnik folder of the home directory
41 | func New() CloudKitKeyManager {
42 | homeDir := homeDir()
43 | components := []string{homeDir, ".sputnik", "secrets"}
44 | secretsFolder := strings.Join(components, "/")
45 |
46 | keyIDFileName := "keyid.txt"
47 | pemFileName := "eckey.pem"
48 |
49 | return NewWithSecretsFolder(secretsFolder, keyIDFileName, pemFileName)
50 | }
51 |
52 | // NewWithSecretsFolder returns a CloudKitKeyManager with a specific secrets folder
53 | // Use this to specify a different storage location from the default
54 | func NewWithSecretsFolder(secretsFolder string, keyIDFileName string, pemFileName string) CloudKitKeyManager {
55 | return CloudKitKeyManager{
56 | secretsFolder: secretsFolder,
57 | pemFileName: pemFileName,
58 | keyIDFileName: keyIDFileName}
59 | }
60 |
61 | // KeyID looks up the CloudKit Key ID
62 | func (c *CloudKitKeyManager) KeyID() string {
63 | keyID := os.Getenv("SPUTNIK_CLOUDKIT_KEYID")
64 | if len(keyID) <= 0 {
65 | // no KeyID found in environment variables
66 | keyIDFromFile, err := c.storedKeyID()
67 | if err != nil {
68 |
69 | }
70 | return keyIDFromFile
71 | }
72 | return keyID
73 | }
74 |
75 | // StoreKeyID stores the given ID to a file in Sputnik's secrets folder
76 | func (c *CloudKitKeyManager) StoreKeyID(key string) error {
77 | path := c.keyIDFilePath()
78 | keyBytes := []byte(key)
79 | return ioutil.WriteFile(path, keyBytes, 0644)
80 | }
81 |
82 | // storedKeyID looks up the Key ID in a file
83 | func (c *CloudKitKeyManager) storedKeyID() (string, error) {
84 | if len(c.inMemoryKeyID) > 0 {
85 | return c.inMemoryKeyID, nil
86 | }
87 |
88 | path := c.keyIDFilePath()
89 | keyBytes, err := ioutil.ReadFile(path)
90 | if err != nil {
91 | return "", err
92 | }
93 | c.inMemoryKeyID = string(keyBytes)
94 |
95 | return c.inMemoryKeyID, nil
96 | }
97 |
98 | // PrivateKey returns the x509 private key that was generated when creating the signing identity
99 | func (c *CloudKitKeyManager) PrivateKey() *ecdsa.PrivateKey {
100 | if c.inMemoryPrivateKey != nil {
101 | return c.inMemoryPrivateKey
102 | }
103 |
104 | inPathPem := c.pemFilePath()
105 | command := exec.Command("openssl", "ec", "-outform", "der", "-in", inPathPem)
106 | bytes, _ := command.Output()
107 |
108 | privateKey, err := x509.ParseECPrivateKey(bytes)
109 | if err != nil {
110 | log.Error("Failed to parse private ec key from pem:")
111 | log.Errorf("%s", err)
112 | return nil
113 | }
114 |
115 | c.inMemoryPrivateKey = privateKey
116 | return c.inMemoryPrivateKey
117 | }
118 |
119 | // PublicKey returns the public key that was generated when creating the signing identity
120 | func (c *CloudKitKeyManager) PublicKey() *ecdsa.PublicKey {
121 | if c.inMemoryPublicKey != nil {
122 | return c.inMemoryPublicKey
123 | }
124 |
125 | var err error
126 | var pub interface{}
127 | pemString := c.PublicKeyString()
128 | pemData := []byte(pemString)
129 | block, _ := pem.Decode(pemData)
130 | if block == nil || block.Type != "PUBLIC KEY" {
131 | err = errors.New("failed to decode PEM block containing public key")
132 | } else {
133 | pub, err = x509.ParsePKIXPublicKey(block.Bytes)
134 |
135 | switch pub := pub.(type) {
136 | case *ecdsa.PublicKey:
137 | c.inMemoryPublicKey = pub
138 | return pub
139 | }
140 | }
141 |
142 | if err != nil {
143 | log.Error("unable to parse public key from certificate:")
144 | log.Errorf("%s", err)
145 | }
146 |
147 | return nil
148 | }
149 |
150 | // PublicKeyString should be named differently. It reads and returns the public part of the PEM encoded signing identity
151 | func (c *CloudKitKeyManager) PublicKeyString() string {
152 | ecKeyPath := c.pemFilePath()
153 |
154 | command := exec.Command("openssl", "ec", "-in", ecKeyPath, "-pubout")
155 | bytes, err := command.Output()
156 | if err != nil {
157 | log.Error("PublicKeyString")
158 | log.Error("Failed to read the public key from PEM")
159 | log.Errorf("%s", err)
160 | }
161 | return string(bytes)
162 | }
163 |
164 | // SigningIdentityExists checks if a signing identity has been created
165 | func (c *CloudKitKeyManager) SigningIdentityExists() (bool, error) {
166 | ecKeyPath := c.pemFilePath()
167 |
168 | file, openError := os.Open(ecKeyPath)
169 | defer file.Close()
170 |
171 | return file != nil && openError == nil, openError
172 | }
173 |
174 | // keyIdFilePath represents the path to the Key ID file
175 | func (c *CloudKitKeyManager) keyIDFilePath() string {
176 | secretsFolder := c.SecretsFolder()
177 | return secretsFolder + "/" + c.keyIDFileName
178 | }
179 |
180 | // pemFilePath represents the path to the PEM encoded certificate
181 | func (c *CloudKitKeyManager) pemFilePath() string {
182 | secretsFolder := c.SecretsFolder()
183 | return secretsFolder + "/" + c.pemFileName
184 | }
185 |
186 | // ECKey should be named differently. It returns the public part of the PEM encoded signing identity
187 | func (c *CloudKitKeyManager) ECKey() string {
188 | ecKeyPath := c.pemFilePath()
189 |
190 | command := exec.Command("openssl", "ec", "-in", ecKeyPath, "-pubout")
191 |
192 | bytes, err := command.Output()
193 | if err != nil {
194 | log.Error("ECKey")
195 | log.Error("Failed to read the public key from PEM")
196 | log.Errorf("%s", err)
197 | }
198 | return string(bytes)
199 | }
200 |
201 | // CreateSigningIdentity creates a new signing identity.
202 | //
203 | // You can paste the signing identity to your iCloud Dashboard when creating a new API Access Key.
204 | func (c *CloudKitKeyManager) CreateSigningIdentity() error {
205 | return c.createPemEncodedCertificate()
206 | }
207 |
208 | // RemoveSigningIdentity removes the existing signing identity
209 | func (c *CloudKitKeyManager) RemoveSigningIdentity() error {
210 | c.inMemoryPrivateKey = nil
211 | c.inMemoryPublicKey = nil
212 |
213 | removePemCommand := exec.Command("rm", c.pemFilePath())
214 | err := removePemCommand.Run()
215 | if err != nil {
216 | log.Error("Unable to remove PEM:")
217 | log.Errorf("%s", err)
218 | }
219 |
220 | removeKeyIDCommand := exec.Command("rm", c.keyIDFilePath())
221 | err = removeKeyIDCommand.Run()
222 |
223 | return err
224 | }
225 |
226 | // createPemEncodedCertificate creates the PEM encoded certificate and stores it
227 | func (c *CloudKitKeyManager) createPemEncodedCertificate() error {
228 | log.Debug("Creating PEM...")
229 | pemFilePath := c.pemFilePath()
230 |
231 | command := exec.Command("openssl", "ecparam", "-name", "prime256v1", "-genkey", "-noout", "-out", pemFilePath)
232 | err := command.Start()
233 | if err != nil {
234 | log.Error("Failed to create pem encoded certificate")
235 | log.Errorf("%s", err)
236 | }
237 |
238 | err = command.Wait()
239 | if err != nil {
240 | log.Error("Error executing `openssl`")
241 | log.Errorf("%s", err)
242 | }
243 |
244 | log.Info("Done creating PEM")
245 |
246 | return err
247 | }
248 |
249 | // SecretsFolder returns the path to Sputnik's secrets folder
250 | func (c *CloudKitKeyManager) SecretsFolder() string {
251 | file, err := os.Open(c.secretsFolder)
252 | defer file.Close()
253 |
254 | if err != nil {
255 | // secrets folder doesn't exist yet, so create it
256 | path, createErr := createSecretsFolder(c.secretsFolder)
257 | if createErr != nil {
258 | log.Debugf("%s", createErr)
259 | }
260 | return path
261 | }
262 |
263 | return file.Name()
264 | }
265 |
266 | func createSecretsFolder(in string) (string, error) {
267 | mode := int(0755)
268 | return in, os.MkdirAll(in, os.FileMode(mode))
269 | }
270 |
271 | func homeDir() string {
272 | usr, err := user.Current()
273 | if err != nil {
274 | // can't get current user
275 | log.Errorf("%s", err)
276 | }
277 |
278 | return usr.HomeDir
279 | }
280 |
--------------------------------------------------------------------------------