Skip to content

Commit b9e55a9

Browse files
authored
Merge pull request #71 from aidenkeating/certificate-pinning-hashes
Include certificate pinning hashes in the service configuration
2 parents 5eec520 + dd14003 commit b9e55a9

File tree

9 files changed

+153
-41
lines changed

9 files changed

+153
-41
lines changed

glide.lock

Lines changed: 26 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

glide.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,6 @@ import:
3939
- package: github.com/olekukonko/tablewriter
4040
version: 65fec0d89a572b4367094e2058d3ebe667de3b60
4141
- package: github.com/mattn/go-runewidth
42-
version: ~0.0.2
42+
version: ~0.0.2
43+
- package: github.com/goreleaser/goreleaser
44+
version: v0.58.0

integration/getClientConfigTestData/no-client-id.golden

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ Usage:
22
mobile get clientconfig <clientID> [flags]
33

44
Flags:
5-
-h, --help help for clientconfig
5+
-h, --help help for clientconfig
6+
--include-cert-pins include certificate hashes for services in the client config
7+
--insecure-skip-tls-verify include certificate hashes for services with invalid/self-signed certificates
68

79
Global Flags:
810
--namespace string --namespace=myproject

integration/getServicesTestData/json-output.golden

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@
55
"metadata": {
66
"name": "1522a4d0e2fbf86a26cbe096eb1b6b2d",
77
"selfLink": "/apis/servicecatalog.k8s.io/v1beta1/clusterserviceclasses/1522a4d0e2fbf86a26cbe096eb1b6b2d",
8-
"uid": "5c206f71-2c6e-11e8-b7c1-0a580a820054",
9-
"resourceVersion": "37621601",
10-
"creationTimestamp": "2018-03-20T18:41:53Z"
8+
"uid": "70e74a1b-37e8-11e8-8387-0242ac110003",
9+
"resourceVersion": "55",
10+
"creationTimestamp": "2018-04-04T09:13:30Z"
1111
},
1212
"spec": {
1313
"clusterServiceBrokerName": "ansible-service-broker",
1414
"externalName": "dh-unifiedpush-apb",
1515
"externalID": "1522a4d0e2fbf86a26cbe096eb1b6b2d",
1616
"description": "AeroGear UnifiedPush Server",
17-
"bindable": false,
17+
"bindable": true,
1818
"binding_retrievable": false,
1919
"planUpdatable": false,
2020
"externalMetadata": {
2121
"dependencies": [
22-
"MySQL:55"
22+
"POSTGRES:95"
2323
],
2424
"displayName": "AeroGear UPS",
2525
"documentationUrl": "https://aerogear.org/push",
@@ -39,9 +39,9 @@
3939
"metadata": {
4040
"name": "2b825339e8d685a78476621a252beea8",
4141
"selfLink": "/apis/servicecatalog.k8s.io/v1beta1/clusterserviceclasses/2b825339e8d685a78476621a252beea8",
42-
"uid": "5c3c2124-2c6e-11e8-b7c1-0a580a820054",
43-
"resourceVersion": "37621606",
44-
"creationTimestamp": "2018-03-20T18:41:54Z"
42+
"uid": "70c8d30c-37e8-11e8-8387-0242ac110003",
43+
"resourceVersion": "37",
44+
"creationTimestamp": "2018-04-04T09:13:29Z"
4545
},
4646
"spec": {
4747
"clusterServiceBrokerName": "ansible-service-broker",
@@ -70,9 +70,9 @@
7070
"metadata": {
7171
"name": "9623d53183cc78619f888ea8499c678e",
7272
"selfLink": "/apis/servicecatalog.k8s.io/v1beta1/clusterserviceclasses/9623d53183cc78619f888ea8499c678e",
73-
"uid": "20abc8b7-1339-11e8-a1f5-0a580a820006",
74-
"resourceVersion": "25057309",
75-
"creationTimestamp": "2018-02-16T16:47:51Z"
73+
"uid": "709e246a-37e8-11e8-8387-0242ac110003",
74+
"resourceVersion": "33",
75+
"creationTimestamp": "2018-04-04T09:13:29Z"
7676
},
7777
"spec": {
7878
"clusterServiceBrokerName": "ansible-service-broker",
@@ -94,16 +94,16 @@
9494
]
9595
},
9696
"status": {
97-
"removedFromBrokerCatalog": true
97+
"removedFromBrokerCatalog": false
9898
}
9999
},
100100
{
101101
"metadata": {
102102
"name": "a0c0c2478554458d5c77abc95f0473a3",
103103
"selfLink": "/apis/servicecatalog.k8s.io/v1beta1/clusterserviceclasses/a0c0c2478554458d5c77abc95f0473a3",
104-
"uid": "5c256f33-2c6e-11e8-b7c1-0a580a820054",
105-
"resourceVersion": "37621605",
106-
"creationTimestamp": "2018-03-20T18:41:53Z"
104+
"uid": "709f0ccd-37e8-11e8-8387-0242ac110003",
105+
"resourceVersion": "35",
106+
"creationTimestamp": "2018-04-04T09:13:29Z"
107107
},
108108
"spec": {
109109
"clusterServiceBrokerName": "ansible-service-broker",
@@ -133,9 +133,9 @@
133133
"metadata": {
134134
"name": "b95513950bb3f132de25d58fb75f8dca",
135135
"selfLink": "/apis/servicecatalog.k8s.io/v1beta1/clusterserviceclasses/b95513950bb3f132de25d58fb75f8dca",
136-
"uid": "20ac53be-1339-11e8-a1f5-0a580a820006",
137-
"resourceVersion": "37164074",
138-
"creationTimestamp": "2018-02-16T16:47:51Z"
136+
"uid": "709da246-37e8-11e8-8387-0242ac110003",
137+
"resourceVersion": "32",
138+
"creationTimestamp": "2018-04-04T09:13:29Z"
139139
},
140140
"spec": {
141141
"clusterServiceBrokerName": "ansible-service-broker",
@@ -160,16 +160,16 @@
160160
]
161161
},
162162
"status": {
163-
"removedFromBrokerCatalog": true
163+
"removedFromBrokerCatalog": false
164164
}
165165
},
166166
{
167167
"metadata": {
168168
"name": "c57e94c36c1e7f6bb41cf7c589d9eb08",
169169
"selfLink": "/apis/servicecatalog.k8s.io/v1beta1/clusterserviceclasses/c57e94c36c1e7f6bb41cf7c589d9eb08",
170-
"uid": "20add41c-1339-11e8-a1f5-0a580a820006",
171-
"resourceVersion": "16418101",
172-
"creationTimestamp": "2018-02-16T16:47:51Z"
170+
"uid": "70aa41b6-37e8-11e8-8387-0242ac110003",
171+
"resourceVersion": "36",
172+
"creationTimestamp": "2018-04-04T09:13:29Z"
173173
},
174174
"spec": {
175175
"clusterServiceBrokerName": "ansible-service-broker",
@@ -192,16 +192,16 @@
192192
]
193193
},
194194
"status": {
195-
"removedFromBrokerCatalog": true
195+
"removedFromBrokerCatalog": false
196196
}
197197
},
198198
{
199199
"metadata": {
200200
"name": "f69b4a4a744c3848d352b7321a8457d1",
201201
"selfLink": "/apis/servicecatalog.k8s.io/v1beta1/clusterserviceclasses/f69b4a4a744c3848d352b7321a8457d1",
202-
"uid": "5c23135c-2c6e-11e8-b7c1-0a580a820054",
203-
"resourceVersion": "37621602",
204-
"creationTimestamp": "2018-03-20T18:41:53Z"
202+
"uid": "709c9f70-37e8-11e8-8387-0242ac110003",
203+
"resourceVersion": "30",
204+
"creationTimestamp": "2018-04-04T09:13:29Z"
205205
},
206206
"spec": {
207207
"clusterServiceBrokerName": "ansible-service-broker",

integration/getServicesTestData/no-args.golden

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
+--------------------------+------------------+--------------------------------+
22
| NAME | INTEGRATIONS | PARAMETERS |
33
+--------------------------+------------------+--------------------------------+
4-
| ups | | MYSQL_DATABASE, |
5-
| | | MYSQL_USER, MYSQL_VERSION, |
6-
| | | _MYSQL_PASSWORD, |
7-
| | | _MYSQL_ROOT_PASSWORD |
4+
| ups | | |
85
| 3scale | | THREESCALE_ACCESS_TOKEN, |
96
| | | THREESCALE_DOMAIN, |
107
| | | THREESCALE_ENABLE_CORS, |

integration/getServicesTestData/table-output.golden

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
+--------------------------+------------------+--------------------------------+
22
| NAME | INTEGRATIONS | PARAMETERS |
33
+--------------------------+------------------+--------------------------------+
4-
| ups | | MYSQL_DATABASE, |
5-
| | | MYSQL_USER, MYSQL_VERSION, |
6-
| | | _MYSQL_PASSWORD, |
7-
| | | _MYSQL_ROOT_PASSWORD |
4+
| ups | | |
85
| 3scale | | THREESCALE_ACCESS_TOKEN, |
96
| | | THREESCALE_DOMAIN, |
107
| | | THREESCALE_ENABLE_CORS, |

pkg/cmd/clientConfig.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ func NewClientConfigCmd(k8Client kubernetes.Interface, mobileClient mobile.Inter
5353

5454
// GetClientConfigCmd returns a cobra command object for getting client configs
5555
func (ccc *ClientConfigCmd) GetClientConfigCmd() *cobra.Command {
56+
var includeCertificatePins bool
57+
var skipTLSVerification bool
58+
5659
cmd := &cobra.Command{
5760
Use: "clientconfig <clientID>",
5861
Short: "get clientconfig returns a client ready filtered configuration of the available services.",
@@ -131,6 +134,18 @@ kubectl plugin mobile get clientconfig`,
131134
ClientID: clientID,
132135
ClusterName: ccc.clusterHost,
133136
}
137+
138+
// If the flag is set then include another key named 'https' which contains certificate hashes.
139+
if includeCertificatePins {
140+
servicePinningHashes, err := retrieveHTTPSConfigForServices(outputJSON.Services, skipTLSVerification)
141+
if err != nil {
142+
return errors.Wrap(err, "Could not append HTTPS configuration for services")
143+
}
144+
outputJSON.Https = &HttpsConfig{
145+
CertificatePinning: servicePinningHashes,
146+
}
147+
}
148+
134149
if err := ccc.Out.Render("get"+cmd.Name(), outputType(cmd.Flags()), outputJSON); err != nil {
135150
return errors.Wrap(err, fmt.Sprintf(output.FailedToOutPutInFormat, "ServiceConfig", outputType(cmd.Flags())))
136151
}
@@ -154,5 +169,8 @@ kubectl plugin mobile get clientconfig`,
154169
table.Render()
155170
return nil
156171
})
172+
173+
cmd.Flags().BoolVar(&skipTLSVerification, "insecure-skip-tls-verify", false, "include certificate hashes for services with invalid/self-signed certificates")
174+
cmd.Flags().BoolVar(&includeCertificatePins, "include-cert-pins", false, "include certificate hashes for services in the client config")
157175
return cmd
158176
}

pkg/cmd/convert.go

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,77 @@
1515
package cmd
1616

1717
import (
18-
"strings"
19-
18+
"crypto/sha256"
19+
"crypto/tls"
20+
"crypto/x509"
21+
"encoding/base64"
22+
"fmt"
23+
"github.com/pkg/errors"
2024
"k8s.io/client-go/pkg/api/v1"
25+
"net/url"
26+
"strings"
2127
)
2228

2329
func isClientConfigKey(key string) bool {
2430
return key == "url" || key == "name" || key == "type" || key == "id"
2531
}
2632

33+
func retrieveHTTPSConfigForServices(svcConfigs []*ServiceConfig, includeInvalidCerts bool) ([]*CertificatePinningHash, error) {
34+
httpsConfig := make([]*CertificatePinningHash, 0)
35+
for _, svc := range svcConfigs {
36+
pinningHash, err := retrieveHTTPSConfigForService(svc, includeInvalidCerts)
37+
if err != nil {
38+
return nil, err
39+
}
40+
if pinningHash != nil {
41+
httpsConfig = append(httpsConfig, pinningHash)
42+
}
43+
}
44+
return httpsConfig, nil
45+
}
46+
47+
func retrieveHTTPSConfigForService(svcConfig *ServiceConfig, allowInvalidCert bool) (*CertificatePinningHash, error) {
48+
// Parse the services URL, if it's not HTTPS then don't attempt to retrieve a cert for it.
49+
serviceURL, err := url.Parse(svcConfig.URL)
50+
if err != nil {
51+
return nil, errors.Wrap(err, "Could not parse service URL "+svcConfig.URL)
52+
}
53+
if serviceURL.Scheme != "https" {
54+
return nil, nil
55+
}
56+
57+
certificate, err := retrieveCertificateForURL(serviceURL, allowInvalidCert)
58+
if err != nil {
59+
return nil, errors.Wrap(err, "Could not retrieve certificate for service URL "+serviceURL.String())
60+
}
61+
62+
hasher := sha256.New()
63+
_, err = hasher.Write(certificate.RawSubjectPublicKeyInfo)
64+
if err != nil {
65+
return nil, errors.Wrap(err, "Could not write public key to buffer for hashing")
66+
}
67+
pinningHash := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
68+
return &CertificatePinningHash{serviceURL.Host, pinningHash}, nil
69+
}
70+
71+
func retrieveCertificateForURL(url *url.URL, allowInvalidCert bool) (*x509.Certificate, error) {
72+
// If the 443 port is not appended to the URLs host then we should append it or tls.Dial will fail.
73+
port := "443"
74+
if url.Port() != "" {
75+
port = url.Port()
76+
}
77+
hostURL := fmt.Sprintf("%s:%s", url.Host, port)
78+
79+
conn, err := tls.Dial("tcp", hostURL, &tls.Config{
80+
InsecureSkipVerify: allowInvalidCert,
81+
})
82+
83+
if err != nil {
84+
return nil, errors.Wrap(err, "Could not retrieve certificate for URL "+url.String())
85+
}
86+
return conn.ConnectionState().PeerCertificates[0], nil
87+
}
88+
2789
func convertSecretToMobileService(s v1.Secret) *Service {
2890
params := map[string]string{}
2991
for key, value := range s.Data {
@@ -32,6 +94,7 @@ func convertSecretToMobileService(s v1.Secret) *Service {
3294
}
3395
}
3496
external := s.Labels["external"] == "true"
97+
3598
return &Service{
3699
Namespace: s.Labels["namespace"],
37100
ID: s.Name,

pkg/cmd/types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type ServiceConfigs struct {
7676
Namespace string `json:"namespace"`
7777
ClientID string `json:"clientId,omitempty"`
7878
Services []*ServiceConfig `json:"services"`
79+
Https *HttpsConfig `json:"https,omitempty"`
7980
}
8081

8182
//ServiceConfig is the configuration for a specific service
@@ -87,6 +88,15 @@ type ServiceConfig struct {
8788
Config map[string]interface{} `json:"config"`
8889
}
8990

91+
type HttpsConfig struct {
92+
CertificatePinning []*CertificatePinningHash `json:"certificatePins,omitempty"`
93+
}
94+
95+
type CertificatePinningHash struct {
96+
Host string `json:"host"`
97+
CertificateHash string `json:"certificateHash"`
98+
}
99+
90100
// defaultSecretConvertor will provide a default secret to config conversion
91101
type defaultSecretConvertor struct{}
92102

0 commit comments

Comments
 (0)