├── .editorconfig
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── cmd
├── lambda-api-authorizer
│ ├── config.go
│ ├── keyprovider.go
│ ├── main.go
│ └── verifier.go
├── lambda-api-client
│ └── main.go
├── lambda-api-server
│ └── main.go
├── lambda-cert-stream
│ └── main.go
├── lambda-revocation-notifier
│ ├── config.go
│ ├── main.go
│ └── template.go
├── lambda-rotate-ca
│ └── main.go
└── ovpn-helper
│ ├── main.go
│ └── utils.go
├── frontend
├── .env
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│ ├── _errors
│ │ └── 403.html
│ ├── favicon.ico
│ ├── favicon.png
│ ├── index.html
│ └── robots.txt
├── src
│ ├── App.vue
│ ├── api
│ │ └── index.js
│ ├── components
│ │ ├── CertificateList.vue
│ │ └── GoogleLogin.vue
│ ├── main.js
│ ├── plugins
│ │ └── vuetify.js
│ ├── router.js
│ ├── store
│ │ ├── index.js
│ │ └── modules
│ │ │ ├── certs.js
│ │ │ └── gauth.js
│ ├── utils
│ │ ├── crypto.js
│ │ └── download.js
│ └── views
│ │ └── Home.vue
└── vue.config.js
├── go.mod
├── go.sum
├── gomod.sh
└── pkg
├── api
├── client
│ ├── api.go
│ ├── cert_delete.go
│ ├── cert_get.go
│ ├── cert_list.go
│ └── cert_put.go
├── server
│ ├── api.go
│ ├── server_config.go
│ ├── server_connect.go
│ ├── server_disconnect.go
│ └── server_verify.go
└── utils.go
├── aws
├── clients.go
├── events.go
├── gsutesecrets.go
└── sign.go
├── gsuite
└── gdirectory.go
├── ovpn
├── config_client.go
├── config_server.go
└── templates.go
└── pki
├── aws
├── aws.go
├── config.go
├── dynamodb.go
├── model.go
└── secrets.go
├── cakey.go
├── cakey_test.go
├── certs.go
├── model.go
├── options.go
├── pki.go
├── statickey.go
└── utils.go
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 |
8 | [frontend/src/**.{js,vue}]
9 | indent_style = space
10 | indent_size = 2
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build clean gomodgen
2 |
3 | build: gomodgen
4 | export GO111MODULE=on
5 | env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/api-authorizer github.com/empathyco/aws-vpn/cmd/lambda-api-authorizer
6 | env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/api-client github.com/empathyco/aws-vpn/cmd/lambda-api-client
7 | env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/api-server github.com/empathyco/aws-vpn/cmd/lambda-api-server
8 | env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/cert-stream github.com/empathyco/aws-vpn/cmd/lambda-cert-stream
9 | env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/revocation-notifier github.com/empathyco/aws-vpn/cmd/lambda-revocation-notifier
10 | env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/rotate-ca github.com/empathyco/aws-vpn/cmd/lambda-rotate-ca
11 | env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/ovpn-helper github.com/empathyco/aws-vpn/cmd/ovpn-helper
12 | clean:
13 | rm -rf ./bin ./vendor Gopkg.lock
14 |
15 | gomodgen:
16 | chmod u+x gomod.sh
17 | ./gomod.sh
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # aws-vpn
2 | A Serverless OpenVPN Certificate Authority running on AWS
3 |
4 | Documentation is coming soon. A detailed introduction is available on the following [article](https://medium.com/empathybroker/build-a-cheaper-more-flexible-vpn-solution-on-aws-with-our-open-source-openvpn-certificate-1a94661ac0af)
5 |
6 |
7 | License
8 | ----
9 |
10 | Copyright 2021 Empathy
11 |
12 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
13 |
14 | https://www.apache.org/licenses/LICENSE-2.0
15 |
16 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
17 |
--------------------------------------------------------------------------------
/cmd/lambda-api-authorizer/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/kelseyhightower/envconfig"
4 |
5 | const kConfigPrefix = "AUTH"
6 |
7 | var configApiAuthorizer struct {
8 | Audience string `split_words:"true" required:"true"`
9 | HostedDomains []string `split_words:"true" required:"true"`
10 | }
11 |
12 | func init() {
13 | envconfig.MustProcess(kConfigPrefix, &configApiAuthorizer)
14 | }
15 |
--------------------------------------------------------------------------------
/cmd/lambda-api-authorizer/keyprovider.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "net/http"
7 | "time"
8 |
9 | "github.com/pkg/errors"
10 | log "github.com/sirupsen/logrus"
11 | jose "gopkg.in/square/go-jose.v2"
12 | )
13 |
14 | const (
15 | kGoogleKeysEndpoint = "https://www.googleapis.com/oauth2/v3/certs"
16 | )
17 |
18 | type KeyProvider interface {
19 | Keys(ctx context.Context) *jose.JSONWebKeySet
20 | }
21 |
22 | type RemoteKeyProvider struct {
23 | endpoint string
24 | client http.Client
25 | keySet *jose.JSONWebKeySet
26 | expires time.Time
27 | }
28 |
29 | func NewRemoteKeyProvider(endpoint string) KeyProvider {
30 | return &RemoteKeyProvider{
31 | endpoint: endpoint,
32 | client: http.Client{
33 | Timeout: 5 * time.Second,
34 | },
35 | }
36 | }
37 |
38 | func NewGoogleKeyProvider() KeyProvider {
39 | return NewRemoteKeyProvider(kGoogleKeysEndpoint)
40 | }
41 |
42 | func (p *RemoteKeyProvider) updateKeySet(ctx context.Context) error {
43 | log.Debugf("Updating JWKs from %s", p.endpoint)
44 |
45 | req, err := http.NewRequest(http.MethodGet, p.endpoint, http.NoBody)
46 | if err != nil {
47 | return errors.WithStack(err)
48 | }
49 |
50 | res, err := p.client.Do(req.WithContext(ctx))
51 | if err != nil {
52 | return errors.WithStack(err)
53 | }
54 | defer res.Body.Close()
55 |
56 | if res.StatusCode != http.StatusOK {
57 | return errors.Errorf("jwk endpoint returned status %d", res.StatusCode)
58 | }
59 |
60 | var keySet *jose.JSONWebKeySet
61 | if err = json.NewDecoder(res.Body).Decode(&keySet); err != nil {
62 | return errors.Wrap(err, "error parsing jwk endpoint response")
63 | }
64 | p.keySet = keySet
65 |
66 | if exp := res.Header.Get("Expires"); exp != "" {
67 | if exptime, err := time.Parse(time.RFC1123, exp); err == nil {
68 | p.expires = exptime.UTC()
69 | } else {
70 | log.WithError(err).Warnf("Error parsing JWKs expiration")
71 | }
72 | }
73 |
74 | log.Debugf("Updated JWKs (%d keys, next update on %s)", len(p.keySet.Keys), p.expires)
75 | return nil
76 | }
77 |
78 | func (p *RemoteKeyProvider) Keys(ctx context.Context) *jose.JSONWebKeySet {
79 | if time.Now().UTC().After(p.expires) {
80 | if err := p.updateKeySet(ctx); err != nil {
81 | log.WithError(err).Error("Error updating JWKs")
82 | }
83 | }
84 |
85 | return p.keySet
86 | }
87 |
--------------------------------------------------------------------------------
/cmd/lambda-api-authorizer/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "os"
7 | "strings"
8 | "time"
9 |
10 | "github.com/aws/aws-lambda-go/events"
11 | "github.com/aws/aws-lambda-go/lambda"
12 | "github.com/aws/aws-sdk-go/aws/arn"
13 | awsservices "github.com/empathybroker/aws-vpn/pkg/aws"
14 | "github.com/empathybroker/aws-vpn/pkg/gsuite"
15 | "github.com/pkg/errors"
16 | log "github.com/sirupsen/logrus"
17 | )
18 |
19 | func init() {
20 | if os.Getenv("DEBUG") == "true" {
21 | log.SetLevel(log.DebugLevel)
22 | }
23 | log.SetFormatter(&log.JSONFormatter{
24 | TimestampFormat: time.RFC3339Nano,
25 | FieldMap: log.FieldMap{
26 | log.FieldKeyTime: "@timestamp",
27 | },
28 | })
29 |
30 | verifier = NewGoogleTokenVerifier(configApiAuthorizer.Audience)
31 | }
32 |
33 | var (
34 | verifier *TokenVerifier
35 |
36 | awsSM = awsservices.NewSecretsManagerClient()
37 | gDirectory = gsuite.NewGoogleDirectory(awsservices.NewAWSServiceAccountProvider(awsSM, "VPN/GoogleServiceAccount"))
38 | )
39 |
40 | type googleClaims struct {
41 | Name string `json:"name"`
42 | Email string `json:"email"`
43 | EmailVerified bool `json:"email_verified"`
44 | HostedDomain string `json:"hd"`
45 | }
46 |
47 | func (c googleClaims) verifyHostedDomain() bool {
48 | for _, hd := range configApiAuthorizer.HostedDomains {
49 | if c.HostedDomain == hd {
50 | return true
51 | }
52 | }
53 | return false
54 | }
55 |
56 | func authHandler(ctx context.Context, input *events.APIGatewayCustomAuthorizerRequest) (*events.APIGatewayCustomAuthorizerResponse, error) {
57 | if input.Type != "TOKEN" {
58 | return nil, errors.New("expected TOKEN authorizer")
59 | }
60 |
61 | method, err := arn.Parse(input.MethodArn)
62 | if err != nil {
63 | return nil, errors.Wrapf(err, "parsing method ARN")
64 | }
65 | log.Debugf("method: %s", method)
66 |
67 | authn := strings.SplitN(input.AuthorizationToken, " ", 2)
68 | if len(authn) != 2 || authn[0] != "Bearer" {
69 | log.Error("Invalid auth header")
70 | return nil, errors.New("Unauthorized")
71 | }
72 |
73 | var extraClaims *googleClaims
74 | jwtClaims, err := verifier.VerifyToken(ctx, authn[1], &extraClaims)
75 | if err != nil {
76 | log.WithError(err).Error("Invalid token")
77 | return nil, errors.New("Unauthorized")
78 | }
79 |
80 | if !extraClaims.EmailVerified {
81 | log.Error("Unverified email")
82 | return nil, errors.New("Unauthorized")
83 | }
84 |
85 | if !extraClaims.verifyHostedDomain() {
86 | log.Errorf("invalid hosted domain: %s", extraClaims.HostedDomain)
87 | return nil, errors.New("Unauthorized")
88 | }
89 |
90 | userInfo, err := gDirectory.GetUserInfo(ctx, jwtClaims.Subject)
91 | if err != nil {
92 | log.WithError(err).Error("Error fetching user info")
93 | return nil, errors.New("Unauthorized")
94 | }
95 |
96 | for k := range userInfo.Schemas {
97 | if k != "VPN" {
98 | delete(userInfo.Schemas, k)
99 | }
100 | }
101 |
102 | googleInfo, err := json.Marshal(userInfo)
103 | if err != nil {
104 | log.WithError(err).Error("Error marshaling Google info")
105 | return nil, errors.New("Unauthorized")
106 | }
107 |
108 | authContext := make(map[string]interface{})
109 | authContext["email"] = extraClaims.Email
110 | authContext["hd"] = extraClaims.HostedDomain
111 | authContext["name"] = extraClaims.Name
112 | authContext["google"] = string(googleInfo)
113 |
114 | var stmts []events.IAMPolicyStatement
115 | stmts = append(stmts, events.IAMPolicyStatement{
116 | Effect: "Allow",
117 | Action: []string{"execute-api:Invoke"},
118 | Resource: []string{"*"},
119 | })
120 |
121 | log.Infof("Authorized: %s", extraClaims.Email)
122 |
123 | return &events.APIGatewayCustomAuthorizerResponse{
124 | PrincipalID: jwtClaims.Subject,
125 | Context: authContext,
126 | PolicyDocument: events.APIGatewayCustomAuthorizerPolicy{
127 | Version: "2012-10-17",
128 | Statement: stmts,
129 | },
130 | }, nil
131 | }
132 |
133 | func main() {
134 | lambda.Start(authHandler)
135 | }
136 |
--------------------------------------------------------------------------------
/cmd/lambda-api-authorizer/verifier.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/pkg/errors"
8 | "gopkg.in/square/go-jose.v2/jwt"
9 | )
10 |
11 | const (
12 | kGoogleIssuer = "accounts.google.com"
13 | )
14 |
15 | type TokenVerifier struct {
16 | keyProvider KeyProvider
17 | expIssuer string
18 | expAudience string
19 | }
20 |
21 | func NewTokenVerifier(keyProvider KeyProvider, iss string, aud string) *TokenVerifier {
22 | return &TokenVerifier{
23 | keyProvider: keyProvider,
24 | expIssuer: iss,
25 | expAudience: aud,
26 | }
27 | }
28 |
29 | func NewGoogleTokenVerifier(aud string) *TokenVerifier {
30 | return NewTokenVerifier(NewGoogleKeyProvider(), kGoogleIssuer, aud)
31 | }
32 |
33 | func (v *TokenVerifier) VerifyToken(ctx context.Context, token string, extra interface{}) (*jwt.Claims, error) {
34 | j, err := jwt.ParseSigned(token)
35 | if err != nil {
36 | return nil, errors.WithStack(err)
37 | }
38 |
39 | var claims *jwt.Claims
40 | if err := j.Claims(v.keyProvider.Keys(ctx), &claims, extra); err == nil {
41 | if err := claims.Validate(jwt.Expected{
42 | Issuer: v.expIssuer,
43 | Audience: []string{v.expAudience},
44 | Time: time.Now().UTC(),
45 | }); err != nil {
46 | return nil, errors.WithStack(err)
47 | }
48 |
49 | return claims, nil
50 | }
51 |
52 | return nil, errors.New("no key found to verify JWT token")
53 | }
54 |
--------------------------------------------------------------------------------
/cmd/lambda-api-client/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "os"
6 | "time"
7 |
8 | "github.com/aws/aws-lambda-go/lambda"
9 | "github.com/awslabs/aws-lambda-go-api-proxy/gorillamux"
10 | clientapi "github.com/empathybroker/aws-vpn/pkg/api/client"
11 | log "github.com/sirupsen/logrus"
12 | )
13 |
14 | func init() {
15 | if os.Getenv("DEBUG") == "true" {
16 | log.SetLevel(log.DebugLevel)
17 | }
18 | log.SetFormatter(&log.JSONFormatter{
19 | TimestampFormat: time.RFC3339Nano,
20 | FieldMap: log.FieldMap{
21 | log.FieldKeyTime: "@timestamp",
22 | },
23 | })
24 | }
25 |
26 | func main() {
27 | router := clientapi.NewRouter()
28 | if _, ok := os.LookupEnv("AWS_LAMBDA_FUNCTION_NAME"); ok {
29 | adapter := gorillamux.New(router)
30 | adapter.StripBasePath("/api/client")
31 | lambda.Start(adapter.Proxy)
32 | return
33 | }
34 |
35 | if err := http.ListenAndServe("localhost:5000", router); err != nil {
36 | log.WithError(err).Fatal("Error serving")
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/cmd/lambda-api-server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "os"
6 | "time"
7 |
8 | "github.com/aws/aws-lambda-go/lambda"
9 | "github.com/awslabs/aws-lambda-go-api-proxy/gorillamux"
10 | serverapi "github.com/empathybroker/aws-vpn/pkg/api/server"
11 | log "github.com/sirupsen/logrus"
12 | )
13 |
14 | func init() {
15 | if os.Getenv("DEBUG") == "true" {
16 | log.SetLevel(log.DebugLevel)
17 | }
18 | log.SetFormatter(&log.JSONFormatter{
19 | TimestampFormat: time.RFC3339Nano,
20 | FieldMap: log.FieldMap{
21 | log.FieldKeyTime: "@timestamp",
22 | },
23 | })
24 | }
25 |
26 | func main() {
27 | router := serverapi.NewRouter()
28 | if _, ok := os.LookupEnv("AWS_LAMBDA_FUNCTION_NAME"); ok {
29 | adapter := gorillamux.New(router)
30 | adapter.StripBasePath("/api/server")
31 | lambda.Start(adapter.Proxy)
32 | return
33 | }
34 |
35 | if err := http.ListenAndServe("localhost:5000", router); err != nil {
36 | log.WithError(err).Fatal("Error serving")
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/cmd/lambda-cert-stream/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "crypto/x509"
6 | "encoding/hex"
7 | "os"
8 | "time"
9 |
10 | "github.com/aws/aws-lambda-go/events"
11 | "github.com/aws/aws-lambda-go/lambda"
12 | awsservices "github.com/empathybroker/aws-vpn/pkg/aws"
13 | "github.com/empathybroker/aws-vpn/pkg/pki"
14 | "github.com/pkg/errors"
15 | log "github.com/sirupsen/logrus"
16 | )
17 |
18 | const (
19 | kAttrSerialNumber = "SerialNumber"
20 | kAttrAuthorityKeyId = "AuthorityKeyId"
21 | kAttrSubjectKeyId = "SubjectKeyId"
22 | kAttrSubjectName = "SubjectName"
23 | kAttrCertType = "CertType"
24 | kAttrIssuedAt = "IssuedAt"
25 | kAttrValidUntil = "ValidUntil"
26 | kAttrRevocationTime = "RevocationTime"
27 | kAttrData = "Data"
28 | )
29 |
30 | var (
31 | snsClient = awsservices.NewSNSClient()
32 | )
33 |
34 | func init() {
35 | if os.Getenv("DEBUG") == "true" {
36 | log.SetLevel(log.DebugLevel)
37 | }
38 | log.SetFormatter(&log.JSONFormatter{
39 | TimestampFormat: time.RFC3339Nano,
40 | FieldMap: log.FieldMap{
41 | log.FieldKeyTime: "@timestamp",
42 | },
43 | })
44 | }
45 |
46 | func parseCertificate(data map[string]events.DynamoDBAttributeValue) (info pki.CertificateInfo, err error) {
47 | defer func() {
48 | if r := recover(); r != nil {
49 | err = errors.Errorf("Panic parsing certificate: %s", r)
50 | }
51 | }()
52 |
53 | for k, v := range data {
54 | switch k {
55 | case kAttrData:
56 | cert, err := x509.ParseCertificate(v.Binary())
57 | if err != nil {
58 | return info, err
59 | }
60 | info.Certificate = cert
61 | case kAttrCertType:
62 | info.CertType = pki.CertType(v.String())
63 | case kAttrSerialNumber:
64 | info.SerialBytes = v.Binary()
65 | info.Serial = hex.EncodeToString(info.SerialBytes)
66 | case kAttrSubjectKeyId:
67 | info.KeyId = hex.EncodeToString(v.Binary())
68 | case kAttrSubjectName:
69 | info.Subject = v.String()
70 | case kAttrIssuedAt:
71 | intv, err := v.Integer()
72 | if err != nil {
73 | return info, err
74 | }
75 | info.NotBefore = time.Unix(intv, 0).UTC()
76 | case kAttrValidUntil:
77 | intv, err := v.Integer()
78 | if err != nil {
79 | return info, err
80 | }
81 | info.NotAfter = time.Unix(intv, 0).UTC()
82 | case kAttrRevocationTime:
83 | intv, err := v.Integer()
84 | if err != nil {
85 | return info, err
86 | }
87 | if intv > 0 {
88 | rt := time.Unix(intv, 0).UTC()
89 | info.Revoked = &rt
90 | }
91 | }
92 | }
93 |
94 | return info, nil
95 | }
96 |
97 | func Handler(ctx context.Context, event events.DynamoDBEvent) error {
98 | for _, record := range event.Records {
99 | event := make(map[string]interface{})
100 |
101 | switch record.EventName {
102 | case string(events.DynamoDBOperationTypeInsert):
103 | event["event"] = "cert_store_new"
104 | case string(events.DynamoDBOperationTypeModify):
105 | event["event"] = "cert_store_updated"
106 | case string(events.DynamoDBOperationTypeRemove):
107 | event["event"] = "cert_store_deleted"
108 | default:
109 | log.Errorf("Unknown operation type: %s", record.EventName)
110 | continue
111 | }
112 |
113 | keySerial, ok := record.Change.Keys[kAttrSerialNumber]
114 | if !ok || keySerial.DataType() != events.DataTypeBinary {
115 | log.Errorf("Missing %s key", kAttrSerialNumber)
116 | continue
117 | }
118 | event["serial"] = hex.EncodeToString(keySerial.Binary())
119 |
120 | if len(record.Change.NewImage) > 0 {
121 | info, err := parseCertificate(record.Change.NewImage)
122 | if err != nil {
123 | log.WithError(err).Errorf("Error parsing new DynamoDB image")
124 | continue
125 | }
126 | event["cert"] = info
127 | }
128 |
129 | if len(record.Change.OldImage) > 0 {
130 | info, err := parseCertificate(record.Change.OldImage)
131 | if err != nil {
132 | log.WithError(err).Errorf("Error parsing old DynamoDB image")
133 | continue
134 | }
135 | event["cert_prev"] = info
136 | }
137 |
138 | if err := awsservices.PublishEvent(snsClient, ctx, event); err != nil {
139 | log.WithError(err).Errorf("Error publishing event")
140 | continue
141 | }
142 | }
143 |
144 | return nil
145 | }
146 |
147 | func main() {
148 | lambda.Start(Handler)
149 | }
150 |
--------------------------------------------------------------------------------
/cmd/lambda-revocation-notifier/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/kelseyhightower/envconfig"
5 | )
6 |
7 | const kConfigPrefix = "NOTIFIER"
8 |
9 | var configNotifier struct {
10 | EmailFrom string `split_words:"true" required:"true"`
11 | EmailSubject string `split_words:"true" default:"VPN Certificate Expiration"`
12 | EmailSignature string `split_words:"true" default:"Your Friendly Ops Team"`
13 | EmailSourceArn string `split_words:"true"`
14 |
15 | AdminURL string `split_words:"true" required:"true"`
16 | HelpURL string `split_words:"true" required:"true"`
17 |
18 | DaysBefore int `split_words:"true" default:"3"`
19 | }
20 |
21 | func init() {
22 | envconfig.MustProcess(kConfigPrefix, &configNotifier)
23 | }
24 |
--------------------------------------------------------------------------------
/cmd/lambda-revocation-notifier/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "io"
7 | "os"
8 | "sort"
9 | "time"
10 |
11 | "github.com/aws/aws-lambda-go/lambda"
12 | "github.com/aws/aws-sdk-go/aws"
13 | "github.com/aws/aws-sdk-go/service/ses"
14 | awsservices "github.com/empathybroker/aws-vpn/pkg/aws"
15 | "github.com/empathybroker/aws-vpn/pkg/pki"
16 | awspki "github.com/empathybroker/aws-vpn/pkg/pki/aws"
17 | "github.com/pkg/errors"
18 | log "github.com/sirupsen/logrus"
19 | )
20 |
21 | func init() {
22 | if os.Getenv("DEBUG") == "true" {
23 | log.SetLevel(log.DebugLevel)
24 | }
25 | log.SetFormatter(&log.JSONFormatter{
26 | TimestampFormat: time.RFC3339Nano,
27 | FieldMap: log.FieldMap{
28 | log.FieldKeyTime: "@timestamp",
29 | },
30 | })
31 | }
32 |
33 | var (
34 | sesClient = awsservices.NewSESClient()
35 | pkiStorage = awspki.NewAWSStorage(awsservices.NewSecretsManagerClient(), awsservices.NewDynamoDBClient())
36 | awsPKI = pki.NewPKI(pkiStorage)
37 | )
38 |
39 | func getEmailBody(w io.Writer, certs []*pki.CertificateInfo) error {
40 | sort.Slice(certs, func(i, j int) bool {
41 | return certs[i].NotAfter.Before(certs[j].NotAfter)
42 | })
43 |
44 | return tplEmail.Execute(w, bodyData{
45 | Certificates: certs,
46 |
47 | AdminURL: configNotifier.AdminURL,
48 | HelpURL: configNotifier.HelpURL,
49 | Signature: configNotifier.EmailSignature,
50 | })
51 | }
52 |
53 | func sendNotificationEmail(ctx context.Context, to string, body string) error {
54 | sesInput := &ses.SendEmailInput{
55 | Source: aws.String(configNotifier.EmailFrom),
56 | Destination: &ses.Destination{
57 | ToAddresses: aws.StringSlice([]string{to}),
58 | },
59 | Message: &ses.Message{
60 | Subject: &ses.Content{
61 | Data: aws.String(configNotifier.EmailSubject),
62 | Charset: aws.String("UTF-8"),
63 | },
64 | Body: &ses.Body{
65 | Text: &ses.Content{
66 | Data: aws.String(body),
67 | Charset: aws.String("UTF-8"),
68 | },
69 | },
70 | },
71 | }
72 |
73 | if configNotifier.EmailSourceArn != "" {
74 | sesInput.SourceArn = aws.String(configNotifier.EmailSourceArn)
75 | }
76 |
77 | r, err := sesClient.SendEmailWithContext(ctx, sesInput)
78 | if err != nil {
79 | return errors.Wrap(err, "sending message with SES")
80 | }
81 |
82 | log.Infof("Sent message to %s with ID %s", to, aws.StringValue(r.MessageId))
83 | return nil
84 | }
85 |
86 | func handler(ctx context.Context) error {
87 | certs, err := awsPKI.ListCerts(ctx, "")
88 | if err != nil {
89 | log.WithError(err).Error("Error listing certificates")
90 | return err
91 | }
92 |
93 | cutoffTime := time.Now().Add(time.Hour * 24 * time.Duration(configNotifier.DaysBefore)).UTC()
94 | log.Debugf("Sending notifications to users with certs expiring before %s", cutoffTime.Format(time.RFC3339))
95 |
96 | targets := make(map[string][]*pki.CertificateInfo)
97 | for _, cert := range certs {
98 | if cert.Revoked != nil {
99 | continue
100 | }
101 |
102 | if cert.NotAfter.Before(cutoffTime) {
103 | targets[cert.Subject] = append(targets[cert.Subject], cert)
104 | }
105 | }
106 |
107 | for to, certs := range targets {
108 | var body bytes.Buffer
109 | if err := getEmailBody(&body, certs); err != nil {
110 | log.WithError(err).Error("Error building email body from template")
111 | continue
112 | }
113 |
114 | if err := sendNotificationEmail(ctx, to, body.String()); err != nil {
115 | log.WithError(err).Error("Error sending email")
116 | continue
117 | }
118 | }
119 |
120 | log.Infof("Sent %d notification emails", len(targets))
121 | return nil
122 | }
123 |
124 | func main() {
125 | lambda.Start(handler)
126 | }
127 |
--------------------------------------------------------------------------------
/cmd/lambda-revocation-notifier/template.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "text/template"
5 |
6 | "github.com/empathybroker/aws-vpn/pkg/pki"
7 | )
8 |
9 | const bodyTemplate = `Hi,
10 |
11 | The following VPN certificates are approaching their expiration time:
12 | {{ range .Certificates }}
13 | * {{ .Serial }} expires on {{ (.NotAfter.Format "02/01/2006 15:04 MST") }}
14 | {{- end }}
15 |
16 | Make sure you request new certificates before then, or you will be unable to connect to the VPN.
17 |
18 | You can manage your certificates at {{ .AdminURL }}
19 |
20 | If you have any questions or need support, please visit {{ .HelpURL }}
21 |
22 | If you are receiving this email in error, please contact us.
23 |
24 | Regards,
25 | - {{ .Signature }}`
26 |
27 | var tplEmail = template.Must(template.New("email_body").Parse(bodyTemplate))
28 |
29 | type bodyData struct {
30 | Certificates []*pki.CertificateInfo
31 |
32 | AdminURL string
33 | HelpURL string
34 | Signature string
35 | }
36 |
--------------------------------------------------------------------------------
/cmd/lambda-rotate-ca/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "os"
7 | "time"
8 |
9 | "github.com/aws/aws-lambda-go/lambda"
10 | "github.com/aws/aws-sdk-go/aws"
11 | "github.com/aws/aws-sdk-go/aws/awserr"
12 | "github.com/aws/aws-sdk-go/aws/session"
13 | "github.com/aws/aws-sdk-go/service/secretsmanager"
14 | "github.com/empathybroker/aws-vpn/pkg/pki"
15 | "github.com/pkg/errors"
16 | log "github.com/sirupsen/logrus"
17 | )
18 |
19 | var (
20 | sess = session.Must(session.NewSession())
21 | secrets = secretsmanager.New(sess)
22 | )
23 |
24 | const (
25 | kStepCreate = "createSecret"
26 | kStepSet = "setSecret"
27 | kStepTest = "testSecret"
28 | kStepFinish = "finishSecret"
29 |
30 | kStageCurrent = "AWSCURRENT"
31 | kStagePending = "AWSPENDING"
32 |
33 | kCAValidity = 90 * 24 * time.Hour
34 | )
35 |
36 | type SecretRotationEvent struct {
37 | ClientRequestToken string `json:"ClientRequestToken"`
38 |
39 | SecretId string `json:"SecretId"`
40 | Step string `json:"Step"`
41 | }
42 |
43 | func init() {
44 | if os.Getenv("DEBUG") == "true" {
45 | log.SetLevel(log.DebugLevel)
46 | }
47 | log.SetFormatter(&log.JSONFormatter{
48 | TimestampFormat: time.RFC3339Nano,
49 | FieldMap: log.FieldMap{
50 | log.FieldKeyTime: "@timestamp",
51 | },
52 | })
53 | }
54 |
55 | func renewCAKey(ctx context.Context, secretId string, caName string, serialNumber string) (pki.CAData, error) {
56 | res, err := secrets.GetSecretValueWithContext(ctx, &secretsmanager.GetSecretValueInput{
57 | SecretId: aws.String(secretId),
58 | VersionStage: aws.String(kStageCurrent),
59 | })
60 | if err != nil {
61 | if awsErr, ok := err.(awserr.Error); ok {
62 | if awsErr.Code() == secretsmanager.ErrCodeResourceNotFoundException {
63 | log.Warnf("Secret is empty. Generating new CA Key")
64 |
65 | return pki.NewCAKey(caName, serialNumber, kCAValidity)
66 | }
67 | } else {
68 | return pki.CAData{}, errors.Wrap(err, "obtaining current key")
69 | }
70 | }
71 |
72 | var oldCA *pki.CAData
73 | if err := json.Unmarshal(res.SecretBinary, &oldCA); err != nil {
74 | log.WithError(err).Errorf("Error unmarshalling current key. New key will not be cross-signed")
75 |
76 | return pki.NewCAKey(caName, serialNumber, kCAValidity)
77 | }
78 |
79 | return oldCA.Renew(caName, serialNumber, kCAValidity)
80 | }
81 |
82 | func Handler(ctx context.Context, event SecretRotationEvent) error {
83 | log.WithFields(log.Fields{
84 | "token": event.ClientRequestToken,
85 | "secret": event.SecretId,
86 | "step": event.Step,
87 | }).Info("Doing rotation step")
88 |
89 | switch event.Step {
90 | case kStepCreate:
91 | caName := os.Getenv("PKI_CA_NAME")
92 | if caName == "" {
93 | return errors.New("Missing environment variable PKI_CA_NAME")
94 | }
95 |
96 | newCA, err := renewCAKey(ctx, event.SecretId, caName, event.ClientRequestToken)
97 | if err != nil {
98 | return errors.Wrap(err, "renewing CA key")
99 | }
100 |
101 | keyData, err := newCA.MarshalJSON()
102 | if err != nil {
103 | return errors.Wrap(err, "encoding new key")
104 | }
105 |
106 | res, err := secrets.PutSecretValueWithContext(ctx, &secretsmanager.PutSecretValueInput{
107 | ClientRequestToken: aws.String(event.ClientRequestToken),
108 | SecretId: aws.String(event.SecretId),
109 | SecretBinary: keyData,
110 | VersionStages: aws.StringSlice([]string{kStagePending}),
111 | })
112 | if err != nil {
113 | if awsErr, ok := err.(awserr.Error); ok {
114 | if awsErr.Code() == secretsmanager.ErrCodeResourceExistsException {
115 | log.Infof("Key already exists. Could be a retry. Skipping error")
116 | return nil
117 | }
118 | }
119 | return errors.Wrap(err, "writing new key")
120 | }
121 |
122 | log.Infof("Created new CA key with version ID %s", aws.StringValue(res.VersionId))
123 | case kStepSet:
124 | // Do nothing
125 | case kStepTest:
126 | // TODO
127 | case kStepFinish:
128 | secretInfo, err := secrets.DescribeSecretWithContext(ctx, &secretsmanager.DescribeSecretInput{
129 | SecretId: aws.String(event.SecretId),
130 | })
131 | if err != nil {
132 | return errors.Wrap(err, "obtaining secret details")
133 | }
134 |
135 | var currentVersion string
136 | for versionId, stages := range secretInfo.VersionIdsToStages {
137 | for _, stage := range aws.StringValueSlice(stages) {
138 | if stage == kStageCurrent {
139 | currentVersion = versionId
140 | }
141 | }
142 | }
143 |
144 | _, err = secrets.UpdateSecretVersionStageWithContext(ctx, &secretsmanager.UpdateSecretVersionStageInput{
145 | SecretId: aws.String(event.SecretId),
146 | RemoveFromVersionId: aws.String(currentVersion),
147 | MoveToVersionId: aws.String(event.ClientRequestToken),
148 | VersionStage: aws.String(kStageCurrent),
149 | })
150 | if err != nil {
151 | return errors.Wrap(err, "error updating secret stage")
152 | }
153 |
154 | log.Info("CA rekeying finished")
155 | default:
156 | log.Errorf("Unknown step: %s", event.Step)
157 | }
158 |
159 | return nil
160 | }
161 |
162 | func main() {
163 | lambda.Start(Handler)
164 | }
165 |
--------------------------------------------------------------------------------
/cmd/ovpn-helper/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 | "os"
10 | "strconv"
11 |
12 | "github.com/empathybroker/aws-vpn/pkg/pki"
13 | log "github.com/sirupsen/logrus"
14 | jose "gopkg.in/square/go-jose.v2"
15 | )
16 |
17 | const (
18 | kConfigLocationEnv = "OPENVPN_CONFIG_FILE"
19 | kServiceUnitEnv = "OPENVPN_SERVICE_UNIT"
20 | )
21 |
22 | func init() {
23 | log.SetFormatter(&log.TextFormatter{
24 | DisableTimestamp: true,
25 | DisableColors: true,
26 | })
27 |
28 | if verb, ok := os.LookupEnv("verb"); ok {
29 | if verb, err := strconv.Atoi(verb); err != nil && verb > 3 {
30 | log.SetLevel(log.DebugLevel)
31 | }
32 | }
33 | }
34 |
35 | func getServerConfig(ctx context.Context) {
36 | log.Debugf("Fetching server certificate")
37 |
38 | configFileName, ok := os.LookupEnv(kConfigLocationEnv)
39 | if !ok {
40 | log.Fatalf("Missing %s environment variable", kConfigLocationEnv)
41 | }
42 |
43 | privKey, err := pki.NewPrivateKey()
44 | if err != nil {
45 | log.WithError(err).Fatal("error generating key")
46 | }
47 |
48 | params := map[string]interface{}{
49 | "publicKey": jose.JSONWebKey{Key: pki.GetPublicKey(privKey)},
50 | }
51 |
52 | var result struct {
53 | Message string `json:"message"`
54 | Config []byte `json:"config"`
55 | }
56 |
57 | status, err := apiRequest(ctx, http.MethodPost, "/server/config", params, &result)
58 | if err != nil {
59 | log.WithError(err).Fatalf("Error making service call")
60 | }
61 |
62 | if status != http.StatusOK {
63 | log.WithField("status", strconv.Itoa(status)).Fatalf("HTTP error: %s", result.Message)
64 | }
65 |
66 | encodedKey, err := pki.EncodePEMPrivateKey(privKey)
67 | if err != nil {
68 | log.WithError(err).Fatal("Error encoding private key")
69 | }
70 |
71 | configData := bytes.ReplaceAll(result.Config, []byte("%PRIVATEKEY%"), encodedKey)
72 | if err := ioutil.WriteFile(configFileName, configData, 0600); err != nil {
73 | log.WithError(err).Fatal("Could not save new configuration file")
74 | }
75 |
76 | if serviceUnit, ok := os.LookupEnv(kServiceUnitEnv); ok {
77 | if err := restartService(ctx, serviceUnit); err != nil {
78 | log.WithError(err).Fatal("Error restarting OpenVPN service")
79 | }
80 | }
81 |
82 | log.Exit(0)
83 | }
84 |
85 | func tlsVerify(ctx context.Context) {
86 | if len(os.Args) != 3 {
87 | log.Fatalf("Invalid arguments")
88 | }
89 |
90 | pathLength, err := strconv.Atoi(os.Args[1])
91 | if err != nil {
92 | log.WithError(err).Fatal("Fist parameter must be a number")
93 | }
94 |
95 | if pathLength > 2 {
96 | log.Fatalf("Certificate path length is too high (got %d, expected <= 2)", pathLength)
97 | }
98 |
99 | if pathLength != 0 {
100 | // We want to wait for the leaf cert
101 | log.Exit(0)
102 | }
103 |
104 | log.Debugf("Validating certificate for %s", os.Args[2])
105 |
106 | params := map[string]interface{}{
107 | "subject": os.Args[2],
108 | "untrusted_ip": os.Getenv("untrusted_ip"),
109 |
110 | "serial": hexFromEnv("tls_serial_hex_0"),
111 | "digest": hexFromEnv("tls_digest_sha256_0"),
112 | }
113 |
114 | var result struct {
115 | Message string `json:"message"`
116 | }
117 |
118 | status, err := apiRequest(ctx, http.MethodPost, "/server/verify", params, &result)
119 | if err != nil {
120 | log.WithError(err).Fatalf("Error making service call")
121 | }
122 |
123 | if status != http.StatusOK {
124 | log.WithField("status", strconv.Itoa(status)).Fatalf("HTTP error: %s", result.Message)
125 | }
126 |
127 | log.Debugf("Certificate validation successful!")
128 | log.Exit(0)
129 | }
130 |
131 | func clientConnect(ctx context.Context) {
132 | if len(os.Args) != 2 {
133 | log.Fatalf("Invalid arguments")
134 | }
135 |
136 | params := map[string]interface{}{
137 | "common_name": os.Getenv("common_name"),
138 | "trusted_ip": os.Getenv("trusted_ip"),
139 |
140 | "client_hwaddr": os.Getenv("IV_HWADDR"),
141 | "client_platform": os.Getenv("IV_PLAT"),
142 | "client_version": os.Getenv("IV_VER"),
143 | "client_gui": os.Getenv("IV_GUI"),
144 | "client_ssl": os.Getenv("IV_SSL"),
145 | }
146 |
147 | var result struct {
148 | Message string `json:"message"`
149 | Push []string `json:"push"`
150 | }
151 |
152 | status, err := apiRequest(ctx, http.MethodPost, "/server/connect", params, &result)
153 | if err != nil {
154 | log.WithError(err).Fatalf("Error making service call")
155 | }
156 |
157 | if status != http.StatusOK {
158 | log.WithField("status", strconv.Itoa(status)).Fatalf("HTTP error: %s", result.Message)
159 | }
160 |
161 | var configData bytes.Buffer
162 | for _, p := range result.Push {
163 | if _, err := fmt.Fprintf(&configData, "push \"%s\"\n", p); err != nil {
164 | log.WithError(err).Fatal("Error writing config data")
165 | }
166 | }
167 |
168 | if err := ioutil.WriteFile(os.Args[1], configData.Bytes(), 0600); err != nil {
169 | log.WithError(err).Fatal("Could not save client configuration file")
170 | }
171 |
172 | log.Exit(0)
173 | }
174 |
175 | func clientDisconnect(ctx context.Context) {
176 | if len(os.Args) != 1 {
177 | log.Fatalf("Invalid arguments")
178 | }
179 |
180 | params := map[string]interface{}{
181 | "common_name": os.Getenv("common_name"),
182 | "trusted_ip": os.Getenv("trusted_ip"),
183 |
184 | "duration": intFromEnv("time_duration"),
185 | "bytes_sent": intFromEnv("bytes_sent"),
186 | "bytes_received": intFromEnv("bytes_received"),
187 |
188 | "client_hwaddr": os.Getenv("IV_HWADDR"),
189 | "client_platform": os.Getenv("IV_PLAT"),
190 | "client_version": os.Getenv("IV_VER"),
191 | "client_gui": os.Getenv("IV_GUI"),
192 | "client_ssl": os.Getenv("IV_SSL"),
193 | }
194 |
195 | var result struct {
196 | Message string `json:"message"`
197 | Push []string `json:"push"`
198 | }
199 |
200 | status, err := apiRequest(ctx, http.MethodPost, "/server/disconnect", params, &result)
201 | if err != nil {
202 | log.WithError(err).Fatalf("Error making service call")
203 | }
204 |
205 | if status != http.StatusOK {
206 | log.WithField("status", strconv.Itoa(status)).Fatalf("HTTP error: %s", result.Message)
207 | }
208 |
209 | log.Exit(0)
210 | }
211 |
212 | func main() {
213 | ctx := context.Background()
214 |
215 | if scriptType, ok := os.LookupEnv("script_type"); ok {
216 | switch scriptType {
217 | case "tls-verify":
218 | tlsVerify(ctx)
219 | case "client-connect":
220 | clientConnect(ctx)
221 | case "client-disconnect":
222 | clientDisconnect(ctx)
223 | default:
224 | log.Fatalf("Unknown script_type: %s", scriptType)
225 | }
226 | log.Exit(1)
227 | }
228 |
229 | if len(os.Args) < 2 {
230 | log.Fatalf("Invalid arguments")
231 | }
232 |
233 | switch os.Args[1] {
234 | case "getconfig":
235 | getServerConfig(ctx)
236 | default:
237 | log.Fatalf("Unknown command %s", os.Args[1])
238 | }
239 |
240 | log.Exit(1)
241 | }
242 |
--------------------------------------------------------------------------------
/cmd/ovpn-helper/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 | "net/http"
9 | "os"
10 | "strconv"
11 | "strings"
12 | "time"
13 |
14 | "github.com/aws/aws-sdk-go/aws/session"
15 | "github.com/coreos/go-systemd/dbus"
16 | awsservices "github.com/empathybroker/aws-vpn/pkg/aws"
17 | "github.com/pkg/errors"
18 | log "github.com/sirupsen/logrus"
19 | "golang.org/x/net/context/ctxhttp"
20 | )
21 |
22 | const (
23 | kJsonContentType = "application/json"
24 | )
25 |
26 | var (
27 | sess = session.Must(session.NewSession())
28 | client = &http.Client{
29 | Transport: awsservices.NewAWSSigner(sess, "execute-api", http.DefaultTransport),
30 | Timeout: 10 * time.Second,
31 | }
32 |
33 | apiBase = os.Getenv("PKI_API_ENDPOINT")
34 | )
35 |
36 | func init() {
37 | if !strings.HasPrefix(apiBase, "https://") {
38 | log.Fatalf("Missing or incorrect PKI_API_ENDPOINT environment variable")
39 | }
40 | }
41 |
42 | func hexFromEnv(key string) string {
43 | return strings.ReplaceAll(os.Getenv(key), ":", "")
44 | }
45 |
46 | func intFromEnv(key string) int {
47 | val, err := strconv.Atoi(os.Getenv(key))
48 | if err != nil {
49 | log.WithError(err).Errorf("Error decoding env int: %s=%s", key, os.Getenv(key))
50 | }
51 | return val
52 | }
53 |
54 | func apiRequest(ctx context.Context, method string, url string, params map[string]interface{}, result interface{}) (int, error) {
55 | var body bytes.Buffer
56 | if params != nil {
57 | if err := json.NewEncoder(&body).Encode(params); err != nil {
58 | return 0, errors.Wrap(err, "could not marshal request")
59 | }
60 | }
61 |
62 | req, err := http.NewRequest(method, apiBase+url, &body)
63 | req.Header.Set("Content-Type", kJsonContentType)
64 |
65 | res, err := ctxhttp.Do(ctx, client, req)
66 | if err != nil {
67 | return 0, errors.Wrapf(err, "error querying service %s", url)
68 | }
69 | defer res.Body.Close()
70 |
71 | if result != nil {
72 | if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
73 | return res.StatusCode, errors.Wrap(err, "error unmarshaling response")
74 | }
75 | }
76 |
77 | return res.StatusCode, nil
78 | }
79 |
80 | func restartService(ctx context.Context, unit string) error {
81 | ctx, _ = context.WithTimeout(ctx, 30*time.Second)
82 |
83 | conn, err := dbus.NewSystemConnection()
84 | if err != nil {
85 | return err
86 | }
87 | defer conn.Close()
88 |
89 | ch := make(chan string)
90 | if _, err := conn.ReloadOrRestartUnit(unit, "replace", ch); err != nil {
91 | return err
92 | }
93 |
94 | select {
95 | case result := <-ch:
96 | switch result {
97 | case "done", "skipped":
98 | log.Debugf("Reloaded service %s", unit)
99 | return nil
100 | default:
101 | return fmt.Errorf("error reloading: %s", result)
102 | }
103 | case <-ctx.Done():
104 | return ctx.Err()
105 | }
106 |
107 | return nil
108 | }
109 |
--------------------------------------------------------------------------------
/frontend/.env:
--------------------------------------------------------------------------------
1 | VUE_APP_NAME=My VPN
2 | VUE_APP_KEY_TYPE=RSA
3 | VUE_APP_GOOGLE_CLIENT_ID=
4 | VUE_APP_GOOGLE_HOSTED_DOMAIN=
5 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | 'extends': [
7 | 'eslint:recommended',
8 | 'plugin:vue/recommended',
9 | '@vue/standard'
10 | ],
11 | rules: {
12 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
14 | },
15 | parserOptions: {
16 | parser: 'babel-eslint'
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw*
22 |
--------------------------------------------------------------------------------
/frontend/.postcssrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | }
--------------------------------------------------------------------------------
/frontend/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vpnadmin",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "axios": "^0.19.0",
12 | "date-fns": "^1.30.1",
13 | "deepmerge": "^4.0.0",
14 | "fibers": "^4.0.1",
15 | "vue": "^2.6.10",
16 | "vue-router": "^3.1.2",
17 | "vuetify": "^2.0.5",
18 | "vuex": "^3.1.1"
19 | },
20 | "devDependencies": {
21 | "@mdi/js": "^3.9.97",
22 | "@vue/cli-plugin-babel": "^3.10.0",
23 | "@vue/cli-plugin-eslint": "^3.10.0",
24 | "@vue/cli-service": "^3.10.0",
25 | "@vue/eslint-config-standard": "^4.0.0",
26 | "babel-eslint": "^10.0.2",
27 | "eslint": "^5.16.0",
28 | "eslint-plugin-vue": "^5.2.3",
29 | "sass": "^1.22.9",
30 | "sass-loader": "^7.2.0",
31 | "vue-cli-plugin-vuetify": "^0.6.3",
32 | "vue-template-compiler": "^2.6.10",
33 | "vuetify-loader": "^1.3.0",
34 | "webpack-bundle-analyzer": "^3.4.1"
35 | },
36 | "browserslist": [
37 | "defaults",
38 | "not dead and > 1%",
39 | "not ie <= 12"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/frontend/public/_errors/403.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/empathyco/platform-aws-vpn/0902aac392cb5fe028f1e3c7abc0515c9a481e4f/frontend/public/_errors/403.html
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/empathyco/platform-aws-vpn/0902aac392cb5fe028f1e3c7abc0515c9a481e4f/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/empathyco/platform-aws-vpn/0902aac392cb5fe028f1e3c7abc0515c9a481e4f/frontend/public/favicon.png
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | <%= VUE_APP_NAME %>
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-Agent: *
2 | Disallow: /
3 |
--------------------------------------------------------------------------------
/frontend/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ appName }}
6 |
7 |
8 |
9 |
10 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
35 |
--------------------------------------------------------------------------------
/frontend/src/api/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import store from '../store'
3 |
4 | let _api = axios.create({
5 | baseURL: '/api/client'
6 | })
7 |
8 | _api.interceptors.request.use((config) => {
9 | let idToken = store.state.gAuth.id_token
10 | if (idToken) {
11 | config.headers.common['Authorization'] = 'Bearer ' + idToken
12 | } else {
13 | delete config.headers.common['Authorization']
14 | }
15 |
16 | return config
17 | })
18 |
19 | export default {
20 | getCerts (allUsers) {
21 | let config = {}
22 | if (allUsers) {
23 | config.params = { 'all': allUsers }
24 | }
25 | return _api.get('/certificates', config)
26 | },
27 | newCert (publicKey) {
28 | return _api.put('/certificates', { 'publicKey': publicKey })
29 | },
30 | revokeCert (serial) {
31 | return _api.delete('/certificates/' + serial)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/frontend/src/components/CertificateList.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | My VPN Certificates
7 |
8 |
9 |
10 |
19 |
20 |
21 |
28 |
29 | $vuetify.icons.refresh
30 |
31 | Refresh
32 |
33 |
39 |
40 | $vuetify.icons.certificate
41 |
42 | Request Certificate
43 |
44 |
45 |
53 |
54 |
64 |
65 |
66 |
67 | {{ item.serial }}
68 |
69 | Key ID: {{ item.keyId }}
70 |
71 |
72 |
73 |
74 |
75 |
76 | {{ item.notBefore | timeDistance }}
77 |
78 | {{ item.notBefore }}
79 |
80 |
81 |
82 |
83 |
84 |
85 | {{ item.notAfter | timeDistance }}
86 |
87 | {{ item.notAfter }}
88 |
89 |
90 |
91 |
92 |
96 |
97 | {{ item.revoked | timeDistance }}
98 |
99 | {{ item.revoked }}
100 |
101 |
107 |
108 | $vuetify.icons.revoke
109 |
110 | Revoke
111 |
112 |
113 |
114 |
121 |
122 | Sign in to continue
123 |
124 |
125 | Loading. Please wait...
126 |
127 |
128 | You have no certificates! Create one by clicking the button above
129 |
130 |
131 |
138 | Your search for "{{ search }}" found no results.
139 |
140 |
141 |
148 |
149 |
150 | Revoke certificate?
151 |
152 | Are you sure you want to revoke certificate
{{ toRevoke.serial }}
153 |
154 |
155 |
159 |
160 | $vuetify.icons.cancel
161 |
162 | Cancel
163 |
164 |
169 |
170 | $vuetify.icons.revoke
171 |
172 | Revoke
173 |
174 |
175 |
176 |
177 |
182 | Certificate downloaded
183 |
187 | $vuetify.icons.close
188 |
189 |
190 |
191 |
192 |
193 |
256 |
--------------------------------------------------------------------------------
/frontend/src/components/GoogleLogin.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
15 | $vuetify.icons.account
16 |
17 |
21 |
22 |
27 |
28 | Logged in as {{ profile.name }} ({{ profile.email }})
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
40 | Sign In
41 |
42 |
46 | Sign Out
47 |
48 |
49 |
50 |
51 |
52 |
53 |
71 |
72 |
73 |
81 |
--------------------------------------------------------------------------------
/frontend/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 | import vuetify from './plugins/vuetify'
4 | import router from './router'
5 | import store from './store'
6 |
7 | Vue.config.productionTip = false
8 |
9 | new Vue({
10 | vuetify,
11 | router,
12 | store,
13 | render: h => h(App)
14 | }).$mount('#app')
15 |
--------------------------------------------------------------------------------
/frontend/src/plugins/vuetify.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuetify from 'vuetify/lib'
3 | import {
4 | mdiAccountCircleOutline,
5 | mdiCancel,
6 | mdiCertificate, mdiClose,
7 | mdiRefresh,
8 | mdiShieldAccountOutline, mdiShieldOffOutline,
9 | mdiTableSearch
10 | } from '@mdi/js'
11 |
12 | Vue.use(Vuetify)
13 |
14 | export default new Vuetify({
15 | icons: {
16 | iconfont: 'mdiSvg',
17 | values: {
18 | account: mdiAccountCircleOutline,
19 | cancel: mdiCancel,
20 | certificate: mdiCertificate,
21 | close: mdiClose,
22 | refresh: mdiRefresh,
23 | revoke: mdiShieldOffOutline,
24 | search: mdiTableSearch,
25 | signIn: mdiShieldAccountOutline
26 | }
27 | }
28 | })
29 |
--------------------------------------------------------------------------------
/frontend/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 |
4 | Vue.use(Router)
5 |
6 | export default new Router({
7 | base: process.env.BASE_URL,
8 | mode: 'history',
9 | routes: [
10 | {
11 | path: '/',
12 | name: 'home',
13 | component: () => import(/* webpackChunkName: "home" */ './views/Home')
14 | },
15 | {
16 | path: '*',
17 | redirect: {
18 | name: 'home'
19 | }
20 | }
21 | ]
22 | })
23 |
--------------------------------------------------------------------------------
/frontend/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | import gauth from './modules/gauth'
5 | import certs from './modules/certs'
6 |
7 | Vue.use(Vuex)
8 |
9 | const store = new Vuex.Store({
10 | strict: process.env.NODE_ENV !== 'production',
11 | modules: {
12 | gAuth: gauth,
13 | certs: certs
14 | },
15 | state: {
16 | alertType: 'error',
17 | alertMessage: '',
18 | alertVisible: false
19 | },
20 | mutations: {
21 | alert: function (state, { alertType, alertMessage }) {
22 | state.alertType = alertType
23 | state.alertMessage = alertMessage
24 | state.alertVisible = true
25 | }
26 | },
27 | actions: {
28 | async displayAlert ({ commit }, alertType, alertMessage) {
29 | await commit('alert', { alertType, alertMessage })
30 | }
31 | }
32 | })
33 |
34 | export default store
35 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/certs.js:
--------------------------------------------------------------------------------
1 |
2 | import api from '../../api'
3 |
4 | import { downloadFile } from '../../utils/download'
5 | import { newKeyPair, exportKeys } from '../../utils/crypto'
6 |
7 | const state = {
8 | certificates: [],
9 | isAdmin: false,
10 | isLoading: false,
11 | downloadedCert: false
12 | }
13 |
14 | const getters = {}
15 |
16 | const actions = {
17 | async updateCerts ({ state, commit }, showAllUsers) {
18 | await commit('setLoading', true)
19 | try {
20 | // TODO: error handling
21 | const r = await api.getCerts(showAllUsers)
22 | r.data.certs = r.data.certs.map(c => {
23 | c.notBefore = new Date(c.notBefore)
24 | c.notAfter = new Date(c.notAfter)
25 |
26 | c.isRevoked = c.revoked !== undefined
27 | if (c.isRevoked) {
28 | c.revoked = new Date(c.revoked)
29 | }
30 | return c
31 | })
32 | await commit('setCertResponse', r.data)
33 | } catch (e) {
34 | console.error(e)
35 | } finally {
36 | await commit('setLoading', false)
37 | }
38 | },
39 | async revokeCert ({ state, commit }, serial) {
40 | try {
41 | // TODO: error handling
42 | await api.revokeCert(serial)
43 | } catch (e) {
44 | console.error(e)
45 | }
46 | },
47 | async getCert ({ state, commit }) {
48 | try {
49 | let keyPair = await newKeyPair()
50 | let jwks = await exportKeys(keyPair)
51 |
52 | let res = await api.newCert(jwks.public)
53 | let config = res.data.replace('%PRIVATEKEY%', jwks.private)
54 | let filename = res.headers['x-vpn-filename'] || `${process.env.VUE_APP_NAME}.ovpn`
55 |
56 | await downloadFile(filename, config, res.headers['content-type'])
57 | await commit('setDownloadedCert', true)
58 | } catch (e) {
59 | console.error(e)
60 | }
61 | },
62 | async clear ({ state, commit }) {
63 | await commit('setCertResponse', { certs: [], isAdmin: false })
64 | await commit('setLoading', false)
65 | }
66 | }
67 |
68 | const mutations = {
69 | setCertResponse (state, data) {
70 | state.certificates = data.certs
71 | state.isAdmin = data.isAdmin
72 | },
73 | setDownloadedCert (state, downloadedCert) {
74 | state.downloadedCert = downloadedCert
75 | },
76 | setLoading (state, isLoading) {
77 | state.isLoading = isLoading
78 | }
79 | }
80 |
81 | export default {
82 | namespaced: true,
83 | state,
84 | getters,
85 | actions,
86 | mutations
87 | }
88 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/gauth.js:
--------------------------------------------------------------------------------
1 |
2 | const state = {
3 | ready: false,
4 | signedIn: false,
5 | id_token: null,
6 | profile: null
7 | }
8 |
9 | const mutations = {
10 | updateUser: (state, user) => {
11 | state.ready = true
12 | if (user.isSignedIn()) {
13 | state.signedIn = true
14 |
15 | let authData = user.getAuthResponse()
16 | state.id_token = authData.id_token
17 |
18 | let profile = user.getBasicProfile()
19 | state.profile = {
20 | id: profile.getId(),
21 | email: profile.getEmail(),
22 | name: profile.getName(),
23 | image: profile.getImageUrl()
24 | }
25 | } else {
26 | state.signedIn = false
27 | state.id_token = null
28 | state.profile = null
29 | }
30 | }
31 | }
32 |
33 | const actions = {
34 | initAuth (state) {
35 | if (!window.gapi) {
36 | console.error('gapi not found')
37 | return
38 | }
39 | window.gapi.load('auth2', () => {
40 | window.gapi.auth2.init({
41 | client_id: process.env.VUE_APP_GOOGLE_CLIENT_ID,
42 | hosted_domain: process.env.VUE_APP_GOOGLE_HOSTED_DOMAIN
43 | }).then(auth2 => {
44 | state.commit('updateUser', auth2.currentUser.get())
45 | auth2.currentUser.listen(user => {
46 | state.commit('updateUser', user)
47 | })
48 | }).catch(err => console.error('error on gauth init', err))
49 | })
50 | },
51 | async signIn () {
52 | try {
53 | let auth = window.gapi.auth2.getAuthInstance()
54 | await auth.signIn()
55 | console.debug('User signed in')
56 | } catch (e) {
57 | console.error(e)
58 | }
59 | },
60 | async signOut () {
61 | try {
62 | let auth = window.gapi.auth2.getAuthInstance()
63 | await auth.signOut()
64 | auth.disconnect()
65 | console.debug('User signed out')
66 | } catch (e) {
67 | console.error(e)
68 | }
69 | }
70 | }
71 |
72 | export default {
73 | namespaced: true,
74 | state,
75 | actions,
76 | mutations
77 | }
78 |
--------------------------------------------------------------------------------
/frontend/src/utils/crypto.js:
--------------------------------------------------------------------------------
1 |
2 | function encodePEM (pemType, ab) {
3 | let b64Encoded = btoa(String.fromCharCode(...new Uint8Array(ab)))
4 | let pemEncoded = b64Encoded.match(/.{1,64}/g).join('\n') + '='.repeat(b64Encoded.length % 4)
5 | return `-----BEGIN ${pemType}-----\n${pemEncoded}\n-----END ${pemType}-----\n`
6 | }
7 |
8 | export async function newKeyPair () {
9 | let keyParams
10 | switch (process.env.VUE_APP_KEY_TYPE) {
11 | case 'RSA':
12 | keyParams = { name: 'RSA-PSS', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }
13 | break
14 | case 'EC':
15 | keyParams = { name: 'ECDSA', namedCurve: 'P-256' }
16 | break
17 | default:
18 | throw Error('invalid Key Type: ' + process.env.VUE_APP_KEY_TYPE)
19 | }
20 |
21 | return window.crypto.subtle.generateKey(keyParams, true, ['sign', 'verify'])
22 | }
23 |
24 | export async function exportKeys (keyPair) {
25 | let publicKey = await window.crypto.subtle.exportKey('jwk', keyPair.publicKey)
26 | let privateKey = await window.crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
27 |
28 | return { 'public': publicKey, 'private': encodePEM(publicKey.kty + ' PRIVATE KEY', privateKey) }
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/src/utils/download.js:
--------------------------------------------------------------------------------
1 |
2 | export function downloadFile (filename, contents, type) {
3 | let blob = new Blob([contents], { 'type': type })
4 | let href = URL.createObjectURL(blob)
5 |
6 | let el = document.createElement('a')
7 | el.setAttribute('href', href)
8 | el.setAttribute('download', filename)
9 | el.click()
10 | setTimeout(() => {
11 | URL.revokeObjectURL(href)
12 | el.remove()
13 | }, 0)
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 | {{ alertMessage }}
14 |
15 |
21 |
25 | $vuetify.icon.signIn
26 |
27 |
33 | Sign In
34 |
35 |
36 |
37 |
38 |
39 |
57 |
--------------------------------------------------------------------------------
/frontend/vue.config.js:
--------------------------------------------------------------------------------
1 |
2 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
3 |
4 | module.exports = {
5 | configureWebpack: {
6 | plugins: [
7 | new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false })
8 | ]
9 | },
10 | devServer: {
11 | proxy: {
12 | '/api': {
13 | target: 'https://replace-me-with-your-vpn-endpoint/api',
14 | changeOrigin: true,
15 | pathRewrite: {
16 | '^/api': ''
17 | }
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/empathybroker/aws-vpn
2 |
3 | require (
4 | cloud.google.com/go v0.36.0 // indirect
5 | github.com/DATA-DOG/go-sqlmock v1.3.3 // indirect
6 | github.com/aws/aws-lambda-go v1.9.0
7 | github.com/aws/aws-sdk-go v1.17.12
8 | github.com/aws/aws-xray-sdk-go v1.0.0-rc.9.0.20190219213013-12231bd5f588
9 | github.com/awslabs/aws-lambda-go-api-proxy v0.2.0
10 | github.com/coreos/go-systemd v0.0.0-20190212144455-93d5ec2c7f76
11 | github.com/godbus/dbus v0.0.0-20181101234600-2ff6f7ffd60f // indirect
12 | github.com/golang/protobuf v1.3.0 // indirect
13 | github.com/google/uuid v1.1.1
14 | github.com/gorilla/mux v1.7.0
15 | github.com/kelseyhightower/envconfig v1.3.0
16 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
17 | github.com/onsi/ginkgo v1.7.0 // indirect
18 | github.com/onsi/gomega v1.4.3 // indirect
19 | github.com/pkg/errors v0.8.1
20 | github.com/sirupsen/logrus v1.3.0
21 | github.com/stretchr/testify v1.3.0 // indirect
22 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 // indirect
23 | golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95
24 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
25 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect
26 | golang.org/x/sys v0.0.0-20190306220723-b294cbcfc56d // indirect
27 | google.golang.org/api v0.1.0
28 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
29 | gopkg.in/square/go-jose.v2 v2.3.0
30 | gopkg.in/yaml.v2 v2.2.2 // indirect
31 | )
32 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
4 | cloud.google.com/go v0.36.0 h1:+aCSj7tOo2LODWVEuZDZeGCckdt6MlSF+X/rB3wUiS8=
5 | cloud.google.com/go v0.36.0/go.mod h1:RUoy9p/M4ge0HzT8L+SDZ8jg+Q6fth0CiBuhFJpSV40=
6 | dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
7 | dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
8 | dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
9 | dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
10 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
11 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
12 | github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
13 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
14 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
15 | github.com/aws/aws-lambda-go v1.9.0 h1:r9TWtk8ozLYdMW+aelUeWny8z2mjghJCMx6/uUwOLNo=
16 | github.com/aws/aws-lambda-go v1.9.0/go.mod h1:zUsUQhAUjYzR8AuduJPCfhBuKWUaDbQiPOG+ouzmE1A=
17 | github.com/aws/aws-sdk-go v1.17.12 h1:jMFwRUaM0LcfdenfvbDLePNoWSoCdOHqF4RCvSB4xNQ=
18 | github.com/aws/aws-sdk-go v1.17.12/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
19 | github.com/aws/aws-xray-sdk-go v1.0.0-rc.9.0.20190219213013-12231bd5f588 h1:GjEy3KyMsasZVD4Gc3aHV4hkP+hOOpYF/UjWflvzP0w=
20 | github.com/aws/aws-xray-sdk-go v1.0.0-rc.9.0.20190219213013-12231bd5f588/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04=
21 | github.com/awslabs/aws-lambda-go-api-proxy v0.2.0 h1:rlPO5+qdErTggV9EVXU3x+mZkX7zWwG9xL6tmX+1c+8=
22 | github.com/awslabs/aws-lambda-go-api-proxy v0.2.0/go.mod h1:1WYCl0lFZD+KAqdW+usdz46oShDhOEj3uTw09Qv++28=
23 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
24 | github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
25 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
26 | github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
27 | github.com/coreos/go-systemd v0.0.0-20190212144455-93d5ec2c7f76 h1:FE783w8WFh+Rvg+7bZ5g8p7gP4SeVS4AoNwkvazlsBg=
28 | github.com/coreos/go-systemd v0.0.0-20190212144455-93d5ec2c7f76/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
32 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
33 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
34 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
35 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
36 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
37 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
38 | github.com/godbus/dbus v0.0.0-20181101234600-2ff6f7ffd60f h1:zlOR3rOlPAVvtfuxGKoghCmop5B0TRyu/ZieziZuGiM=
39 | github.com/godbus/dbus v0.0.0-20181101234600-2ff6f7ffd60f/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
40 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
41 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
42 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
43 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
44 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
45 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
46 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
47 | github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
48 | github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
49 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
50 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
51 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
52 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
53 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
54 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
55 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
56 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
57 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
58 | github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
59 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
60 | github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U=
61 | github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
62 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
63 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
64 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
65 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
66 | github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
67 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
68 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
69 | github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM=
70 | github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
71 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
72 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
73 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
74 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
75 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
76 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
77 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
78 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
79 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
80 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
81 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
82 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
83 | github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
84 | github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
85 | github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
86 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
87 | github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
88 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
89 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
90 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
91 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
92 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
93 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
94 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
95 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
96 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
97 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
98 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
99 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
100 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
101 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
102 | github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
103 | github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
104 | github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
105 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
106 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
107 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
108 | github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
109 | github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
110 | github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
111 | github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
112 | github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
113 | github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
114 | github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
115 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
116 | github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
117 | github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
118 | github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
119 | github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
120 | github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
121 | github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
122 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
123 | github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
124 | github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
125 | github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
126 | github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
127 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
128 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
129 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
130 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
131 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
132 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
133 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
134 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
135 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
136 | go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
137 | golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
138 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
139 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
140 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU=
141 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
142 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
143 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
144 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
145 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
146 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
147 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
148 | golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
149 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
150 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
151 | golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU=
152 | golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
153 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
154 | golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
155 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
156 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
157 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
158 | golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
159 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
160 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
161 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
162 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
163 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
164 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
165 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
166 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
167 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
168 | golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
169 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
170 | golang.org/x/sys v0.0.0-20190306220723-b294cbcfc56d h1:4Ew1XHJYjwX6RiE8SgSymqS1zCRQyGpcAnVfbpEuXfE=
171 | golang.org/x/sys v0.0.0-20190306220723-b294cbcfc56d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
172 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
173 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
174 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
175 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
176 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
177 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
178 | golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
179 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
180 | google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
181 | google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI=
182 | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
183 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
184 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
185 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
186 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
187 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
188 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
189 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
190 | google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
191 | google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
192 | google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4=
193 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
194 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
195 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
196 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
197 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
198 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
199 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
200 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
201 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
202 | gopkg.in/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U=
203 | gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
204 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
205 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
206 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
207 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
208 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
209 | grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
210 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
211 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
212 | sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
213 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
214 |
--------------------------------------------------------------------------------
/gomod.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -eu
3 |
4 | touch go.mod
5 |
6 | PROJECT_NAME=$(basename $(pwd | xargs dirname))
7 | CURRENT_DIR=$(basename $(pwd))
8 |
9 | CONTENT=$(cat <<-EOD
10 | module github.com/${PROJECT_NAME}/${CURRENT_DIR}
11 |
12 | require github.com/aws/aws-lambda-go v1.6.0
13 | EOD
14 | )
15 |
16 | echo "$CONTENT" > go.mod
17 |
--------------------------------------------------------------------------------
/pkg/api/client/api.go:
--------------------------------------------------------------------------------
1 | package clientapi
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/aws/aws-xray-sdk-go/xray"
7 | awsservices "github.com/empathybroker/aws-vpn/pkg/aws"
8 | "github.com/empathybroker/aws-vpn/pkg/pki"
9 | awspki "github.com/empathybroker/aws-vpn/pkg/pki/aws"
10 | "github.com/gorilla/mux"
11 | )
12 |
13 | var (
14 | pkiStorage = awspki.NewAWSStorage(awsservices.NewSecretsManagerClient(), awsservices.NewDynamoDBClient())
15 | apiPKI = pki.NewPKI(pkiStorage)
16 | apiSNS = awsservices.NewSNSClient()
17 | )
18 |
19 | func NewRouter() *mux.Router {
20 | r := mux.NewRouter()
21 | r.Use(func(handler http.Handler) http.Handler {
22 | return xray.Handler(xray.NewFixedSegmentNamer("vpn-api-client"), handler)
23 | })
24 |
25 | r.HandleFunc("/certificates", apiGetCerts).Methods(http.MethodGet)
26 | r.HandleFunc("/certificates", apiNewCert).Methods(http.MethodPut)
27 | r.HandleFunc("/certificates/{serial}", apiGetCert).Methods(http.MethodGet)
28 | r.HandleFunc("/certificates/{serial}", apiRevokeCert).Methods(http.MethodDelete)
29 |
30 | return r
31 | }
32 |
--------------------------------------------------------------------------------
/pkg/api/client/cert_delete.go:
--------------------------------------------------------------------------------
1 | package clientapi
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/empathybroker/aws-vpn/pkg/api"
7 | awsservices "github.com/empathybroker/aws-vpn/pkg/aws"
8 | "github.com/empathybroker/aws-vpn/pkg/pki"
9 | "github.com/gorilla/mux"
10 | log "github.com/sirupsen/logrus"
11 | )
12 |
13 | func apiRevokeCert(w http.ResponseWriter, r *http.Request) {
14 | vars := mux.Vars(r)
15 |
16 | serial, err := pki.DecodeSerial(vars["serial"])
17 | if err != nil {
18 | api.ErrorResponse(w, http.StatusBadRequest, err, "Invalid serial")
19 | return
20 | }
21 |
22 | _, userInfo, err := api.GetAPIGWPrincipal(r)
23 | if err != nil {
24 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error obtaining principal")
25 | return
26 | }
27 |
28 | cert, err := apiPKI.GetCertBySerial(r.Context(), serial)
29 | if err != nil {
30 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error obtaining certificate")
31 | return
32 | }
33 |
34 | if cert == nil || (!userInfo.IsAdmin && cert.Subject != userInfo.Email) {
35 | api.ErrorResponse(w, http.StatusNotFound, nil, "Not Found")
36 | return
37 | }
38 |
39 | cert, err = apiPKI.RevokeCert(r.Context(), serial)
40 | if err != nil {
41 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error revoking certificate")
42 | return
43 | }
44 |
45 | if cert == nil {
46 | api.ErrorResponse(w, http.StatusNotFound, nil, "Not found")
47 | return
48 | }
49 |
50 | event := api.J{
51 | "event": "cert_revoked",
52 | "revoked_by": userInfo.Email,
53 | "cert": cert,
54 | }
55 |
56 | if err := awsservices.PublishEvent(apiSNS, r.Context(), event); err != nil {
57 | log.WithError(err).Error("Error publishing event")
58 | }
59 |
60 | api.JsonResponse(w, http.StatusOK, cert)
61 | }
62 |
--------------------------------------------------------------------------------
/pkg/api/client/cert_get.go:
--------------------------------------------------------------------------------
1 | package clientapi
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/empathybroker/aws-vpn/pkg/api"
7 |
8 | "github.com/empathybroker/aws-vpn/pkg/pki"
9 | "github.com/gorilla/mux"
10 | )
11 |
12 | func apiGetCert(w http.ResponseWriter, r *http.Request) {
13 | vars := mux.Vars(r)
14 |
15 | serial, err := pki.DecodeSerial(vars["serial"])
16 | if err != nil {
17 | api.ErrorResponse(w, http.StatusBadRequest, err, "Invalid serial")
18 | return
19 | }
20 |
21 | cert, err := apiPKI.GetCertBySerial(r.Context(), serial)
22 | if err != nil {
23 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error obtaining certificate")
24 | return
25 | }
26 |
27 | _, userInfo, err := api.GetAPIGWPrincipal(r)
28 | if err != nil {
29 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error obtaining principal")
30 | return
31 | }
32 |
33 | if cert == nil || cert.Subject != userInfo.Email {
34 | api.ErrorResponse(w, http.StatusNotFound, nil, "Not found")
35 | return
36 | }
37 |
38 | api.JsonResponse(w, http.StatusOK, cert)
39 | }
40 |
--------------------------------------------------------------------------------
/pkg/api/client/cert_list.go:
--------------------------------------------------------------------------------
1 | package clientapi
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/empathybroker/aws-vpn/pkg/api"
7 | )
8 |
9 | func apiGetCerts(w http.ResponseWriter, r *http.Request) {
10 | _, userInfo, err := api.GetAPIGWPrincipal(r)
11 | if err != nil {
12 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error obtaining principal")
13 | return
14 | }
15 |
16 | subject := userInfo.Email
17 | if userInfo.IsAdmin && r.URL.Query().Get("all") == "true" {
18 | subject = ""
19 | }
20 |
21 | certs, err := apiPKI.ListCerts(r.Context(), subject)
22 | if err != nil {
23 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error listing certs")
24 | return
25 | }
26 |
27 | api.JsonResponse(w, http.StatusOK, api.J{
28 | "isAdmin": userInfo.IsAdmin,
29 | "certs": certs,
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/pkg/api/client/cert_put.go:
--------------------------------------------------------------------------------
1 | package clientapi
2 |
3 | import (
4 | "bytes"
5 | "crypto/x509/pkix"
6 | "encoding/asn1"
7 | "encoding/json"
8 | "fmt"
9 | "net/http"
10 | "os"
11 | "time"
12 |
13 | "github.com/empathybroker/aws-vpn/pkg/api"
14 |
15 | awsservices "github.com/empathybroker/aws-vpn/pkg/aws"
16 | "github.com/empathybroker/aws-vpn/pkg/ovpn"
17 | "github.com/empathybroker/aws-vpn/pkg/pki"
18 | log "github.com/sirupsen/logrus"
19 | jose "gopkg.in/square/go-jose.v2"
20 | )
21 |
22 | const (
23 | kMaxClientCerts = 2
24 | )
25 |
26 | type newCertRequest struct {
27 | PublicKey jose.JSONWebKey `json:"publicKey"`
28 | }
29 |
30 | func apiNewCert(w http.ResponseWriter, r *http.Request) {
31 | var request newCertRequest
32 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
33 | api.ErrorResponse(w, http.StatusBadRequest, err, "Invalid input")
34 | return
35 | }
36 |
37 | if !request.PublicKey.Valid() {
38 | api.ErrorResponse(w, http.StatusBadRequest, nil, "Invalid public key")
39 | return
40 | }
41 |
42 | _, userInfo, err := api.GetAPIGWPrincipal(r)
43 | if err != nil {
44 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error obtaining principal")
45 | return
46 | }
47 |
48 | currentCerts, err := apiPKI.ListCerts(r.Context(), userInfo.Email)
49 | if err != nil {
50 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error listing certs")
51 | return
52 | }
53 |
54 | nonRevoked := 0
55 | for _, cCert := range currentCerts {
56 | if cCert.Revoked == nil {
57 | nonRevoked += 1
58 |
59 | if nonRevoked >= kMaxClientCerts {
60 | if _, err := apiPKI.RevokeCert(r.Context(), cCert.SerialBytes); err != nil {
61 | log.WithError(err).Error("Error revoking certificate")
62 | // Don't fail
63 | }
64 | }
65 | }
66 | }
67 |
68 | name := pkix.Name{
69 | CommonName: userInfo.Email,
70 | }
71 |
72 | /*name.ExtraNames = append(name.ExtraNames, pkix.AttributeTypeAndValue{
73 | Type: asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 3}, // mail
74 | Value: userInfo.Email},
75 | )*/
76 | name.ExtraNames = append(name.ExtraNames, pkix.AttributeTypeAndValue{
77 | Type: asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 1}, // uid
78 | Value: userInfo.Id},
79 | )
80 |
81 | cert, err := apiPKI.CreateCertificate(r.Context(), request.PublicKey.Key, name,
82 | pki.WithDuration(30*24*time.Hour), pki.ClientCert, pki.WithEmail(userInfo.Email))
83 | if err != nil {
84 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error creating certificate")
85 | return
86 | }
87 |
88 | event := api.J{
89 | "event": "cert_signed",
90 | "cert": cert,
91 | }
92 |
93 | if err := awsservices.PublishEvent(apiSNS, r.Context(), event); err != nil {
94 | log.WithError(err).Error("Error publishing event")
95 | }
96 |
97 | configData := ovpn.ConfigData{
98 | Certificate: cert.Certificate,
99 |
100 | CACert: apiPKI.GetCACert(r.Context()),
101 | PrevCACert: apiPKI.GetPrevCACert(r.Context()),
102 | CrossCert: apiPKI.GetCrossCert(r.Context()),
103 |
104 | StaticKey: apiPKI.GetStaticKey(r.Context()),
105 | }
106 |
107 | var buf bytes.Buffer
108 | if err := ovpn.GetClientConfig(&buf, configData); err != nil {
109 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error writing OpenVPN profile")
110 | return
111 | }
112 |
113 | fname := "OpenVPN"
114 | if caName := os.Getenv("PKI_CLIENT_CERT_NAME"); caName != "" {
115 | fname = caName
116 | }
117 |
118 | w.Header().Set("X-VPN-Filename", fmt.Sprintf("%s.ovpn", fname))
119 | w.Header().Set("Content-Type", "application/x-openvpn-profile")
120 | w.WriteHeader(http.StatusOK)
121 | if _, err := w.Write(buf.Bytes()); err != nil {
122 | log.WithError(err).Error("Error writing binary response")
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/pkg/api/server/api.go:
--------------------------------------------------------------------------------
1 | package serverapi
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/aws/aws-xray-sdk-go/xray"
7 | awsservices "github.com/empathybroker/aws-vpn/pkg/aws"
8 | "github.com/empathybroker/aws-vpn/pkg/gsuite"
9 | "github.com/empathybroker/aws-vpn/pkg/pki"
10 | awspki "github.com/empathybroker/aws-vpn/pkg/pki/aws"
11 | "github.com/gorilla/mux"
12 | )
13 |
14 | var (
15 | pkiStorage = awspki.NewAWSStorage(awsservices.NewSecretsManagerClient(), awsservices.NewDynamoDBClient())
16 | apiPKI = pki.NewPKI(pkiStorage)
17 | apiSNS = awsservices.NewSNSClient()
18 | apiEC2 = awsservices.NewEC2Client()
19 |
20 | apiSecretsManager = awsservices.NewSecretsManagerClient()
21 | apiDirectory = gsuite.NewGoogleDirectory(awsservices.NewAWSServiceAccountProvider(apiSecretsManager, "VPN/GoogleServiceAccount"))
22 | )
23 |
24 | func NewRouter() *mux.Router {
25 | r := mux.NewRouter()
26 | r.Use(func(handler http.Handler) http.Handler {
27 | return xray.Handler(xray.NewFixedSegmentNamer("vpn-api-server"), handler)
28 | })
29 |
30 | r.HandleFunc("/config", apiServerConfig).Methods(http.MethodPost)
31 | r.HandleFunc("/verify", apiServerVerify).Methods(http.MethodPost)
32 | r.HandleFunc("/connect", apiServerConnect).Methods(http.MethodPost)
33 | r.HandleFunc("/disconnect", apiServerDisconnect).Methods(http.MethodPost)
34 |
35 | return r
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/api/server/server_config.go:
--------------------------------------------------------------------------------
1 | package serverapi
2 |
3 | import (
4 | "bytes"
5 | "crypto/x509/pkix"
6 | "encoding/json"
7 | "net/http"
8 | "os"
9 | "time"
10 |
11 | "github.com/empathybroker/aws-vpn/pkg/api"
12 | awsservices "github.com/empathybroker/aws-vpn/pkg/aws"
13 | "github.com/empathybroker/aws-vpn/pkg/ovpn"
14 | "github.com/empathybroker/aws-vpn/pkg/pki"
15 | log "github.com/sirupsen/logrus"
16 | jose "gopkg.in/square/go-jose.v2"
17 | )
18 |
19 | type configRequest struct {
20 | PublicKey jose.JSONWebKey `json:"publicKey"`
21 | }
22 |
23 | func apiServerConfig(w http.ResponseWriter, r *http.Request) {
24 | var request configRequest
25 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
26 | api.ErrorResponse(w, http.StatusBadRequest, err, "Invalid input")
27 | return
28 | }
29 |
30 | if !request.PublicKey.Valid() {
31 | api.ErrorResponse(w, http.StatusBadRequest, nil, "Invalid public key")
32 | return
33 | }
34 |
35 | dnsName := os.Getenv("PKI_DOMAIN")
36 | if dnsName == "" {
37 | api.ErrorResponse(w, http.StatusInternalServerError, nil, "Missing domain name")
38 | return
39 | }
40 |
41 | cert, err := apiPKI.CreateCertificate(r.Context(), request.PublicKey.Key,
42 | pkix.Name{CommonName: dnsName}, pki.ServerCert,
43 | pki.WithDuration(30*24*time.Hour), pki.WithDNS(dnsName))
44 | if err != nil {
45 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error signing certificate")
46 | return
47 | }
48 |
49 | configData := ovpn.ConfigData{
50 | Certificate: cert.Certificate,
51 |
52 | CACert: apiPKI.GetCACert(r.Context()),
53 | PrevCACert: apiPKI.GetPrevCACert(r.Context()),
54 | CrossCert: apiPKI.GetCrossCert(r.Context()),
55 |
56 | StaticKey: apiPKI.GetStaticKey(r.Context()),
57 | }
58 |
59 | var config bytes.Buffer
60 | if err := ovpn.GetServerConfig(&config, configData); err != nil {
61 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error obtaining config")
62 | return
63 | }
64 |
65 | event := api.J{
66 | "event": "server_cert",
67 | "success": true,
68 | "request": request,
69 | "cert": cert,
70 | }
71 |
72 | if err := awsservices.PublishEvent(apiSNS, r.Context(), event); err != nil {
73 | log.WithError(err).Error("Error publishing event")
74 | }
75 |
76 | api.JsonResponse(w, http.StatusOK, api.J{
77 | "message": "OK",
78 | "config": config.Bytes(),
79 | })
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/api/server/server_connect.go:
--------------------------------------------------------------------------------
1 | package serverapi
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net"
7 | "net/http"
8 | "os"
9 | "strings"
10 |
11 | "github.com/aws/aws-sdk-go/aws"
12 | "github.com/aws/aws-sdk-go/service/ec2"
13 | "github.com/empathybroker/aws-vpn/pkg/api"
14 | awsservices "github.com/empathybroker/aws-vpn/pkg/aws"
15 | "github.com/pkg/errors"
16 | log "github.com/sirupsen/logrus"
17 | )
18 |
19 | type connectRequest struct {
20 | CommonName string `json:"common_name"`
21 | TrustedIP net.IP `json:"trusted_ip"`
22 |
23 | ClientHWAddr string `json:"client_hwaddr"`
24 | ClientPlatform string `json:"client_platform"`
25 | ClientVersion string `json:"client_version"`
26 | ClientGUI string `json:"client_gui"`
27 | ClientSSL string `json:"client_ssl"`
28 | }
29 |
30 | func subnetToRoute(subnetStr string) (string, error) {
31 | if _, subnet, err := net.ParseCIDR(subnetStr); err == nil {
32 | mask := net.IPv4(subnet.Mask[0], subnet.Mask[1], subnet.Mask[2], subnet.Mask[3])
33 | return fmt.Sprintf("route %s %s", subnet.IP.String(), mask.String()), nil
34 | } else {
35 | return "", errors.Wrapf(err, "parsing subnet CIDR: %s", subnetStr)
36 | }
37 | }
38 |
39 | func apiServerConnect(w http.ResponseWriter, r *http.Request) {
40 | var request connectRequest
41 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
42 | api.ErrorResponse(w, http.StatusBadRequest, err, "Invalid input")
43 | return
44 | }
45 |
46 | userInfo, err := apiDirectory.GetUserInfo(r.Context(), request.CommonName)
47 | if err != nil {
48 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Could not get user info")
49 | return
50 | }
51 |
52 | if userInfo.IsSuspended {
53 | event := api.J{
54 | "event": "client_connect",
55 | "success": false,
56 | "error": "user_suspended",
57 | "request": request,
58 | }
59 |
60 | if err := awsservices.PublishEvent(apiSNS, r.Context(), event); err != nil {
61 | log.WithError(err).Error("Error publishing event")
62 | }
63 |
64 | api.ErrorResponse(w, http.StatusUnauthorized, nil, "User is suspended")
65 | return
66 | }
67 |
68 | var push []string
69 | if vpnSchema, ok := userInfo.Schemas["VPN"]; ok {
70 | if macaddrs, ok := vpnSchema["Allowed_MACs"].([]interface{}); ok && len(macaddrs) > 0 {
71 |
72 | found := false
73 | for _, macaddrI := range macaddrs {
74 | if macaddr, ok := macaddrI.(string); ok {
75 | if macaddr == request.ClientHWAddr {
76 | found = true
77 | break
78 | }
79 | }
80 | }
81 |
82 | if !found {
83 | event := api.J{
84 | "event": "client_connect",
85 | "success": false,
86 | "error": "mac_mismatch",
87 | "expected": macaddrs,
88 | "request": request,
89 | }
90 |
91 | if err := awsservices.PublishEvent(apiSNS, r.Context(), event); err != nil {
92 | log.WithError(err).Error("Error publishing event")
93 | }
94 |
95 | api.ErrorResponse(w, http.StatusUnauthorized, nil, "Unexpected MAC address")
96 | return
97 | }
98 | }
99 |
100 | if subnets, ok := vpnSchema["Allowed_subnets"].([]interface{}); ok {
101 | for _, subnetI := range subnets {
102 | if subnetStr, ok := subnetI.(string); ok {
103 | if route, err := subnetToRoute(subnetStr); err == nil {
104 | push = append(push, route)
105 | } else {
106 | log.WithError(err).Error("Error parsing route from user schema")
107 | }
108 | }
109 | }
110 | }
111 | } else {
112 | // error missing schema
113 | }
114 |
115 | if os.Getenv("PKI_ROUTE_EC2_PREFIX_LIST") == "true" {
116 | if pl, err := apiEC2.DescribePrefixListsWithContext(r.Context(), &ec2.DescribePrefixListsInput{}); err == nil {
117 | for _, p := range pl.PrefixLists {
118 | for _, subnet := range aws.StringValueSlice(p.Cidrs) {
119 | if route, err := subnetToRoute(subnet); err == nil {
120 | push = append(push, route)
121 | } else {
122 | log.WithError(err).Error("Error parsing route from prefix list")
123 | }
124 | }
125 | }
126 | }
127 | }
128 |
129 | if domainSearch := os.Getenv("PKI_DOMAIN_SEARCH"); domainSearch != "" {
130 | for _, domain := range strings.Split(domainSearch, ",") {
131 | push = append(push, fmt.Sprintf("dhcp-option DOMAIN %s", domain))
132 | }
133 | }
134 |
135 | push = append(push, "dhcp-option DNS 10.8.0.1")
136 | push = append(push, "route-metric 101")
137 |
138 | event := api.J{
139 | "event": "client_connect",
140 | "success": true,
141 | "request": request,
142 | "push": push,
143 | }
144 |
145 | if err := awsservices.PublishEvent(apiSNS, r.Context(), event); err != nil {
146 | log.WithError(err).Error("Error publishing event")
147 | }
148 |
149 | api.JsonResponse(w, http.StatusOK, api.J{
150 | "message": "OK",
151 | "push": push,
152 | })
153 | }
154 |
--------------------------------------------------------------------------------
/pkg/api/server/server_disconnect.go:
--------------------------------------------------------------------------------
1 | package serverapi
2 |
3 | import (
4 | "encoding/json"
5 | "net"
6 | "net/http"
7 |
8 | "github.com/empathybroker/aws-vpn/pkg/api"
9 | awsservices "github.com/empathybroker/aws-vpn/pkg/aws"
10 | log "github.com/sirupsen/logrus"
11 | )
12 |
13 | type disconnectRequest struct {
14 | CommonName string `json:"common_name"`
15 | TrustedIP net.IP `json:"trusted_ip"`
16 |
17 | Duration int `json:"duration"`
18 | BytesSent int `json:"bytes_sent"`
19 | BytesReceived int `json:"bytes_received"`
20 |
21 | ClientHWAddr string `json:"client_hwaddr"`
22 | ClientPlatform string `json:"client_platform"`
23 | ClientVersion string `json:"client_version"`
24 | ClientGUI string `json:"client_gui"`
25 | ClientSSL string `json:"client_ssl"`
26 | }
27 |
28 | func apiServerDisconnect(w http.ResponseWriter, r *http.Request) {
29 | var request disconnectRequest
30 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
31 | api.ErrorResponse(w, http.StatusBadRequest, err, "Invalid input")
32 | return
33 | }
34 |
35 | event := api.J{
36 | "event": "client_disconnect",
37 | "request": request,
38 | }
39 |
40 | if err := awsservices.PublishEvent(apiSNS, r.Context(), event); err != nil {
41 | log.WithError(err).Error("Error publishing event")
42 | }
43 |
44 | api.JsonResponse(w, http.StatusOK, api.J{"message": "OK"})
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/api/server/server_verify.go:
--------------------------------------------------------------------------------
1 | package serverapi
2 |
3 | import (
4 | "encoding/json"
5 | "net"
6 | "net/http"
7 |
8 | "github.com/empathybroker/aws-vpn/pkg/api"
9 | awsservices "github.com/empathybroker/aws-vpn/pkg/aws"
10 | "github.com/empathybroker/aws-vpn/pkg/pki"
11 | log "github.com/sirupsen/logrus"
12 | )
13 |
14 | type verifyRequest struct {
15 | Subject string `json:"subject"`
16 | Serial string `json:"serial"`
17 | Digest string `json:"digest"`
18 | Client net.IP `json:"client"`
19 | }
20 |
21 | func apiServerVerify(w http.ResponseWriter, r *http.Request) {
22 | var request verifyRequest
23 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
24 | api.ErrorResponse(w, http.StatusBadRequest, err, "Invalid input")
25 | return
26 | }
27 |
28 | serial, err := pki.DecodeSerial(request.Serial)
29 | if err != nil {
30 | api.ErrorResponse(w, http.StatusBadRequest, err, "Invalid serial")
31 | return
32 | }
33 |
34 | cert, err := apiPKI.GetCertBySerial(r.Context(), serial)
35 | if err != nil {
36 | api.ErrorResponse(w, http.StatusInternalServerError, err, "Error fetching certificate")
37 | return
38 | }
39 |
40 | if cert == nil {
41 | event := api.J{
42 | "event": "cert_verify",
43 | "success": false,
44 | "error": "cert_not_found",
45 | "request": request,
46 | }
47 |
48 | if err := awsservices.PublishEvent(apiSNS, r.Context(), event); err != nil {
49 | log.WithError(err).Error("Error publishing event")
50 | }
51 |
52 | api.ErrorResponse(w, http.StatusUnauthorized, err, "Certificate not found")
53 | return
54 | }
55 |
56 | if cert.Revoked != nil {
57 | event := api.J{
58 | "event": "cert_verify",
59 | "success": false,
60 | "error": "cert_revoked",
61 | "request": request,
62 | }
63 |
64 | if err := awsservices.PublishEvent(apiSNS, r.Context(), event); err != nil {
65 | log.WithError(err).Error("Error publishing event")
66 | }
67 |
68 | api.ErrorResponse(w, http.StatusForbidden, err, "Certificate has been revoked")
69 | return
70 | }
71 |
72 | event := api.J{
73 | "event": "cert_verify",
74 | "success": true,
75 | "request": request,
76 | "cert": cert,
77 | }
78 |
79 | if err := awsservices.PublishEvent(apiSNS, r.Context(), event); err != nil {
80 | log.WithError(err).Error("Error publishing event")
81 | }
82 |
83 | api.JsonResponse(w, http.StatusOK, api.J{"message": "OK"})
84 | }
85 |
--------------------------------------------------------------------------------
/pkg/api/utils.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "os"
7 |
8 | "github.com/awslabs/aws-lambda-go-api-proxy/core"
9 | "github.com/empathybroker/aws-vpn/pkg/gsuite"
10 | "github.com/pkg/errors"
11 | log "github.com/sirupsen/logrus"
12 | )
13 |
14 | var (
15 | accessor core.RequestAccessor
16 | )
17 |
18 | type J map[string]interface{}
19 |
20 | func GetAPIGWPrincipal(r *http.Request) (string, *gsuite.UserInfo, error) {
21 | ctx, err := accessor.GetAPIGatewayContext(r)
22 | if err != nil {
23 | return "", nil, errors.WithStack(err)
24 | }
25 |
26 | pid, ok := ctx.Authorizer["principalId"].(string)
27 | if !ok || pid == "" {
28 | return "", nil, errors.New("principalID not found")
29 | }
30 |
31 | googleInfo, ok := ctx.Authorizer["google"].(string)
32 | if !ok || googleInfo == "" {
33 | return "", nil, errors.New("Google data not found")
34 | }
35 |
36 | var userInfo *gsuite.UserInfo
37 | if err := json.Unmarshal([]byte(googleInfo), &userInfo); err != nil {
38 | return "", nil, errors.Wrap(err, "unmarshaling Google data")
39 | }
40 |
41 | return pid, userInfo, nil
42 | }
43 |
44 | func JsonResponse(w http.ResponseWriter, statusCode int, value interface{}) {
45 | w.Header().Set("Content-Type", "application/json")
46 | w.WriteHeader(statusCode)
47 | if err := json.NewEncoder(w).Encode(value); err != nil {
48 | log.WithError(err).Errorf("Error writing JSON response")
49 | }
50 | }
51 |
52 | func ErrorResponse(w http.ResponseWriter, statusCode int, err error, msg string) {
53 | e := J{"message": msg}
54 |
55 | if err != nil && os.Getenv("DEBUG") == "true" {
56 | e["error"] = err.Error()
57 | e["cause"] = errors.Cause(err)
58 | }
59 |
60 | log.WithError(err).Errorf("Error response with code %d: %s", statusCode, msg)
61 | JsonResponse(w, statusCode, e)
62 | }
63 |
--------------------------------------------------------------------------------
/pkg/aws/clients.go:
--------------------------------------------------------------------------------
1 | package awsservices
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/aws/aws-sdk-go/service/ses/sesiface"
8 |
9 | "github.com/aws/aws-sdk-go/aws"
10 | "github.com/aws/aws-sdk-go/aws/credentials/stscreds"
11 | "github.com/aws/aws-sdk-go/aws/session"
12 | "github.com/aws/aws-sdk-go/service/dynamodb"
13 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
14 | "github.com/aws/aws-sdk-go/service/ec2"
15 | "github.com/aws/aws-sdk-go/service/ec2/ec2iface"
16 | "github.com/aws/aws-sdk-go/service/secretsmanager"
17 | "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
18 | "github.com/aws/aws-sdk-go/service/ses"
19 | "github.com/aws/aws-sdk-go/service/sns"
20 | "github.com/aws/aws-sdk-go/service/sns/snsiface"
21 | "github.com/aws/aws-xray-sdk-go/xray"
22 | )
23 |
24 | var (
25 | sess = session.Must(session.NewSession())
26 | )
27 |
28 | func makeConfig(serviceName string) *aws.Config {
29 | config := aws.NewConfig()
30 | if role, ok := os.LookupEnv(fmt.Sprintf("AWS_%s_ROLE_ARN", serviceName)); ok {
31 | config.WithCredentials(stscreds.NewCredentials(sess, role))
32 | }
33 |
34 | if endpoint, ok := os.LookupEnv(fmt.Sprintf("AWS_%s_ENDPOINT", serviceName)); ok {
35 | config.WithDisableSSL(true)
36 | config.WithEndpoint(endpoint)
37 | }
38 |
39 | return config
40 | }
41 |
42 | func NewDynamoDBClient() dynamodbiface.DynamoDBAPI {
43 | svc := dynamodb.New(sess, makeConfig("DYNAMODB"))
44 | xray.AWS(svc.Client)
45 | return svc
46 | }
47 |
48 | func NewSecretsManagerClient() secretsmanageriface.SecretsManagerAPI {
49 | svc := secretsmanager.New(sess, makeConfig("SECRETSMANAGER"))
50 | xray.AWS(svc.Client)
51 | return svc
52 | }
53 |
54 | func NewSNSClient() snsiface.SNSAPI {
55 | svc := sns.New(sess, makeConfig("SNS"))
56 | xray.AWS(svc.Client)
57 | return svc
58 | }
59 |
60 | func NewEC2Client() ec2iface.EC2API {
61 | svc := ec2.New(sess, makeConfig("EC2"))
62 | xray.AWS(svc.Client)
63 | return svc
64 | }
65 |
66 | func NewSESClient() sesiface.SESAPI {
67 | svc := ses.New(sess, makeConfig("SES"))
68 | xray.AWS(svc.Client)
69 | return svc
70 | }
71 |
--------------------------------------------------------------------------------
/pkg/aws/events.go:
--------------------------------------------------------------------------------
1 | package awsservices
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "os"
7 |
8 | "github.com/aws/aws-sdk-go/aws"
9 | "github.com/aws/aws-sdk-go/service/sns"
10 | "github.com/aws/aws-sdk-go/service/sns/snsiface"
11 | "github.com/pkg/errors"
12 | log "github.com/sirupsen/logrus"
13 | )
14 |
15 | func PublishEvent(snsClient snsiface.SNSAPI, ctx context.Context, message map[string]interface{}) error {
16 | if topicARN, ok := os.LookupEnv("SNS_TOPIC_ARN"); ok {
17 | jsonMsg, err := json.Marshal(message)
18 | if err != nil {
19 | return errors.Wrapf(err, "marshalling event")
20 | }
21 |
22 | r, err := snsClient.PublishWithContext(ctx, &sns.PublishInput{
23 | TopicArn: aws.String(topicARN),
24 | Subject: aws.String("VPN Event"),
25 | Message: aws.String(string(jsonMsg)),
26 | })
27 | if err != nil {
28 | return errors.Wrap(err, "sending event")
29 | }
30 |
31 | log.Debugf("Published event with ID: %s", aws.StringValue(r.MessageId))
32 | }
33 |
34 | return nil
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/aws/gsutesecrets.go:
--------------------------------------------------------------------------------
1 | package awsservices
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/aws/aws-sdk-go/aws"
7 | "github.com/aws/aws-sdk-go/service/secretsmanager"
8 | "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
9 | )
10 |
11 | type awsServiceAccountProvider struct {
12 | secretsManager secretsmanageriface.SecretsManagerAPI
13 | secretId string
14 | }
15 |
16 | func NewAWSServiceAccountProvider(secretsManager secretsmanageriface.SecretsManagerAPI, secretId string) *awsServiceAccountProvider {
17 | return &awsServiceAccountProvider{
18 | secretsManager: secretsManager,
19 | secretId: secretId,
20 | }
21 | }
22 |
23 | func (p awsServiceAccountProvider) GetKey(ctx context.Context) ([]byte, error) {
24 | res, err := p.secretsManager.GetSecretValueWithContext(ctx, &secretsmanager.GetSecretValueInput{
25 | SecretId: aws.String(p.secretId),
26 | })
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | return res.SecretBinary, nil
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/aws/sign.go:
--------------------------------------------------------------------------------
1 | package awsservices
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "io/ioutil"
7 | "net/http"
8 | "time"
9 |
10 | "github.com/aws/aws-sdk-go/aws"
11 | "github.com/aws/aws-sdk-go/aws/session"
12 | "github.com/aws/aws-sdk-go/aws/signer/v4"
13 | )
14 |
15 | type awsSigningTransport struct {
16 | parent http.RoundTripper
17 | signer *v4.Signer
18 | serviceName string
19 | serviceRegion string
20 | }
21 |
22 | func NewAWSSigner(s *session.Session, serviceName string, parent http.RoundTripper) *awsSigningTransport {
23 | return &awsSigningTransport{
24 | signer: v4.NewSigner(s.Config.Credentials),
25 | serviceRegion: aws.StringValue(s.Config.Region),
26 | serviceName: serviceName,
27 | parent: parent,
28 | }
29 | }
30 |
31 | func (t *awsSigningTransport) RoundTrip(req *http.Request) (*http.Response, error) {
32 | req = cloneRequest(req)
33 |
34 | var body io.ReadSeeker
35 | if req.Body != nil {
36 | defer req.Body.Close()
37 | payload, err := ioutil.ReadAll(req.Body)
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | body = bytes.NewReader(payload)
43 | req.Body = ioutil.NopCloser(body)
44 | }
45 |
46 | _, err := t.signer.Sign(req, body, t.serviceName, t.serviceRegion, time.Now())
47 | if err != nil {
48 | return nil, err
49 | }
50 |
51 | return t.parent.RoundTrip(req)
52 | }
53 |
54 | // CloneRequest creates a shallow copy of the request along with a deep copy of the Headers.
55 | func cloneRequest(req *http.Request) *http.Request {
56 | r := new(http.Request)
57 |
58 | // shallow clone
59 | *r = *req
60 |
61 | // deep copy headers
62 | r.Header = cloneHeader(req.Header)
63 |
64 | return r
65 | }
66 |
67 | // CloneHeader creates a deep copy of an http.Header.
68 | func cloneHeader(in http.Header) http.Header {
69 | out := make(http.Header, len(in))
70 | for key, values := range in {
71 | newValues := make([]string, len(values))
72 | copy(newValues, values)
73 | out[key] = newValues
74 | }
75 | return out
76 | }
77 |
--------------------------------------------------------------------------------
/pkg/gsuite/gdirectory.go:
--------------------------------------------------------------------------------
1 | package gsuite
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "os"
7 | "sync"
8 |
9 | "github.com/aws/aws-xray-sdk-go/xray"
10 | log "github.com/sirupsen/logrus"
11 | "golang.org/x/oauth2/google"
12 | admin "google.golang.org/api/admin/directory/v1"
13 | )
14 |
15 | type GoogleServiceAccountSecretProvider interface {
16 | GetKey(context.Context) ([]byte, error)
17 | }
18 |
19 | type GoogleDirectory struct {
20 | secretProvider GoogleServiceAccountSecretProvider
21 |
22 | service *admin.Service
23 | once sync.Once
24 | }
25 |
26 | type UserInfo struct {
27 | Id string `json:"id"`
28 | Email string `json:"email"`
29 | Name string `json:"name"`
30 |
31 | IsAdmin bool `json:"is_admin"`
32 | IsSuspended bool `json:"is_suspended"`
33 |
34 | Schemas map[string]map[string]interface{} `json:"schemas"`
35 | }
36 |
37 | func NewGoogleDirectory(secretProvider GoogleServiceAccountSecretProvider) *GoogleDirectory {
38 | return &GoogleDirectory{
39 | secretProvider: secretProvider,
40 | }
41 | }
42 |
43 | func (d *GoogleDirectory) init(ctx context.Context) {
44 | d.once.Do(func() {
45 | jsonKey, err := d.secretProvider.GetKey(ctx)
46 | if err != nil {
47 | log.WithError(err).Fatal("Error obtaining Google Service Account key")
48 | }
49 |
50 | config, err := google.JWTConfigFromJSON(jsonKey, admin.AdminDirectoryUserReadonlyScope)
51 | if err != nil {
52 | log.WithError(err).Fatal("Error creating config from Service Account key")
53 | }
54 |
55 | if iSubject, ok := os.LookupEnv("GSUITE_IMPERSONATE_SUBJECT"); ok {
56 | config.Subject = iSubject
57 | }
58 |
59 | oaClient := config.Client(ctx)
60 | oaClient = xray.Client(oaClient)
61 | d.service, err = admin.New(oaClient)
62 | if err != nil {
63 | log.WithError(err).Fatal("Error creating Admin SDK client")
64 | }
65 | })
66 | }
67 |
68 | func (d *GoogleDirectory) GetUserInfo(ctx context.Context, userKey string) (*UserInfo, error) {
69 | d.init(ctx)
70 |
71 | res, err := d.service.Users.Get(userKey).Projection("full").Context(ctx).Do()
72 | if err != nil {
73 | return nil, err
74 | }
75 |
76 | schemas := make(map[string]map[string]interface{})
77 | for s, r := range res.CustomSchemas {
78 | schemas[s] = make(map[string]interface{})
79 | var schema map[string]interface{}
80 | if err := json.Unmarshal(r, &schema); err == nil {
81 | for k, v := range schema {
82 | if v1, ok := v.([]interface{}); ok {
83 | var values []interface{}
84 | for _, v2 := range v1 {
85 | if v3, ok := v2.(map[string]interface{}); ok {
86 | if v4, ok := v3["value"]; ok {
87 | values = append(values, v4)
88 | }
89 | } else {
90 | values = append(values, v3)
91 | }
92 | }
93 | schemas[s][k] = values
94 | } else {
95 | schemas[s][k] = v
96 | }
97 | }
98 | }
99 | }
100 |
101 | return &UserInfo{
102 | Id: res.Id,
103 | Email: res.PrimaryEmail,
104 | Name: res.Name.FullName,
105 |
106 | IsAdmin: res.IsAdmin,
107 | IsSuspended: res.Suspended,
108 |
109 | Schemas: schemas,
110 | }, nil
111 | }
112 |
--------------------------------------------------------------------------------
/pkg/ovpn/config_client.go:
--------------------------------------------------------------------------------
1 | package ovpn
2 |
3 | const kClientConfigTemplate = `# OpenVPN client settings
4 | # Configuration file for {{ .Certificate.Subject.String }}
5 | # Expires on: {{ .Certificate.NotAfter.Format "02 Jan 06 15:04 MST" }}
6 |
7 | dev tun
8 | client
9 | remote {{ env "PKI_DOMAIN" }}
10 | remote-random-hostname
11 | push-peer-info
12 | explicit-exit-notify
13 |
14 | remote-cert-tls server
15 | tls-version-min 1.3 or-highest
16 | verify-x509-name '{{ env "PKI_DOMAIN" }}' name
17 | cipher AES-256-GCM
18 | auth SHA256
19 | verb 3
20 |
21 | # Serial Number: {{ .Certificate.SerialNumber.Text 16 }}
22 |
23 | {{ printf "%s" (pemCert .Certificate) -}}
24 |
25 |
26 | {{ if .CrossCert -}}
27 |
28 | {{ printf "%s" (pemCert .CrossCert) -}}
29 |
30 | {{- end }}
31 |
32 | # Key ID: {{ printf "%x" .Certificate.SubjectKeyId }}
33 |
34 | %PRIVATEKEY%
35 |
36 |
37 | {{ printf "%s" (pemCert .CACert) -}}
38 | {{- if .PrevCACert -}}{{ printf "%s" (pemCert .PrevCACert) -}}{{- end -}}
39 |
40 |
41 |
42 | {{ printf "%s" .StaticKey -}}
43 |
44 | `
45 |
--------------------------------------------------------------------------------
/pkg/ovpn/config_server.go:
--------------------------------------------------------------------------------
1 | package ovpn
2 |
3 | const kServerConfigTemplate = `# OpenVPN server settings
4 | # Expires on: {{ .Certificate.NotAfter.Format "02 Jan 06 15:04 MST" }}
5 |
6 | dev tun
7 | topology subnet
8 | server 10.8.0.0 255.255.255.0
9 | keepalive 10 60
10 | client-to-client
11 | push-peer-info
12 | opt-verify
13 | mlock
14 |
15 | auth-gen-token
16 | explicit-exit-notify
17 | script-security 2
18 | status-version 3
19 | mute-replay-warnings
20 | verb 4
21 |
22 | # scripts
23 | setenv AWS_REGION {{ env "AWS_REGION" }}
24 | setenv PKI_API_ENDPOINT https://{{ env "PKI_DOMAIN" }}/api
25 | tls-verify /usr/local/bin/ovpn-helper
26 | client-connect /usr/local/bin/ovpn-helper
27 | client-disconnect /usr/local/bin/ovpn-helper
28 |
29 | # Authentication
30 | tls-server
31 | auth SHA256
32 | cipher AES-256-GCM
33 | remote-cert-tls client
34 | tls-cert-profile preferred
35 | tls-version-min 1.3 or-highest
36 | x509-username-field ext:subjectAltName
37 |
38 | # Serial Number {{ .Certificate.SerialNumber.Text 16 }}
39 |
40 | {{ printf "%s" (pemCert .Certificate) -}}
41 |
42 |
43 | {{ if .CrossCert -}}
44 |
45 | {{ printf "%s" (pemCert .CrossCert) -}}
46 |
47 | {{- end }}
48 |
49 | # Key ID: {{ printf "%x" .Certificate.SubjectKeyId }}
50 |
51 | %PRIVATEKEY%
52 |
53 |
54 | {{ printf "%s" (pemCert .CACert) -}}
55 | {{- if .PrevCACert -}}{{ printf "%s" (pemCert .PrevCACert) -}}{{- end -}}
56 |
57 |
58 |
59 | {{ printf "%s" .StaticKey -}}
60 |
61 |
62 | dh none
63 | `
64 |
--------------------------------------------------------------------------------
/pkg/ovpn/templates.go:
--------------------------------------------------------------------------------
1 | package ovpn
2 |
3 | import (
4 | "crypto/x509"
5 | "io"
6 | "os"
7 | "text/template"
8 |
9 | "github.com/empathybroker/aws-vpn/pkg/pki"
10 | )
11 |
12 | var (
13 | funcs = template.FuncMap{
14 | "pemCert": pki.EncodePEMCert,
15 | "env": os.Getenv,
16 | }
17 | tplClientConfig = template.Must(template.New("client_config").Funcs(funcs).Parse(kClientConfigTemplate))
18 | tplServerConfig = template.Must(template.New("server_config").Funcs(funcs).Parse(kServerConfigTemplate))
19 | )
20 |
21 | type ConfigData struct {
22 | Certificate *x509.Certificate
23 |
24 | CACert *x509.Certificate
25 | PrevCACert *x509.Certificate
26 | CrossCert *x509.Certificate
27 |
28 | StaticKey pki.StaticKey
29 | }
30 |
31 | func GetClientConfig(w io.Writer, data ConfigData) error {
32 | return tplClientConfig.Execute(w, data)
33 | }
34 |
35 | func GetServerConfig(w io.Writer, data ConfigData) error {
36 | return tplServerConfig.Execute(w, data)
37 | }
38 |
--------------------------------------------------------------------------------
/pkg/pki/aws/aws.go:
--------------------------------------------------------------------------------
1 | package awspki
2 |
3 | import (
4 | "sync"
5 | "time"
6 |
7 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
8 | "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
9 | "github.com/empathybroker/aws-vpn/pkg/pki"
10 | )
11 |
12 | type awsStorage struct {
13 | sm secretsmanageriface.SecretsManagerAPI
14 | ddb dynamodbiface.DynamoDBAPI
15 |
16 | data pki.CAData
17 | mut sync.Mutex
18 | exp time.Time
19 | }
20 |
21 | func NewAWSStorage(sm secretsmanageriface.SecretsManagerAPI, ddb dynamodbiface.DynamoDBAPI) *awsStorage {
22 | return &awsStorage{
23 | sm: sm,
24 | ddb: ddb,
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/pki/aws/config.go:
--------------------------------------------------------------------------------
1 | package awspki
2 |
3 | import (
4 | "github.com/kelseyhightower/envconfig"
5 | )
6 |
7 | const kConfigPrefix = "PKI_AWS"
8 |
9 | var configAWSPKI struct {
10 | SecretName string `split_words:"true" default:"VPN/CAPrivateKey"`
11 | TableName string `split_words:"true" default:"vpn_certificates"`
12 | DurationDays int `split_words:"true" default:"30"`
13 | }
14 |
15 | func init() {
16 | envconfig.MustProcess(kConfigPrefix, &configAWSPKI)
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/pki/aws/dynamodb.go:
--------------------------------------------------------------------------------
1 | package awspki
2 |
3 | import (
4 | "context"
5 | "crypto/x509"
6 | "errors"
7 | "time"
8 |
9 | "github.com/aws/aws-sdk-go/aws"
10 | "github.com/aws/aws-sdk-go/service/dynamodb"
11 | A "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
12 | E "github.com/aws/aws-sdk-go/service/dynamodb/expression"
13 | "github.com/empathybroker/aws-vpn/pkg/pki"
14 | log "github.com/sirupsen/logrus"
15 | )
16 |
17 | func (s *awsStorage) GetCertBySerial(ctx context.Context, serial []byte) (*pki.CertificateInfo, error) {
18 | res, err := s.ddb.GetItemWithContext(ctx, &dynamodb.GetItemInput{
19 | TableName: aws.String(configAWSPKI.TableName),
20 | Key: map[string]*dynamodb.AttributeValue{
21 | kAttrSerialNumber: {B: serial},
22 | },
23 | ConsistentRead: aws.Bool(true),
24 | })
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | var certEntry dynamoCertEntry
30 | if err := A.UnmarshalMap(res.Item, &certEntry); err != nil {
31 | return nil, err
32 | }
33 |
34 | return certEntry.toCertificateInfo()
35 | }
36 |
37 | func (s *awsStorage) ListAllCerts(ctx context.Context) ([]*pki.CertificateInfo, error) {
38 | filter := E.GreaterThan(E.Key(kAttrValidUntil), E.Value(time.Now().UTC().Unix()))
39 | filter = filter.And(E.Equal(E.Key(kAttrCertType), E.Value(pki.CertTypeClient)))
40 |
41 | exp, err := E.NewBuilder().
42 | WithFilter(filter).
43 | Build()
44 | if err != nil {
45 | return nil, err
46 | }
47 |
48 | query := &dynamodb.ScanInput{
49 | TableName: aws.String(configAWSPKI.TableName),
50 |
51 | ExpressionAttributeNames: exp.Names(),
52 | ExpressionAttributeValues: exp.Values(),
53 | FilterExpression: exp.Filter(),
54 | }
55 |
56 | certs := make([]*pki.CertificateInfo, 0)
57 | if err := s.ddb.ScanPagesWithContext(ctx, query, func(output *dynamodb.ScanOutput, b bool) bool {
58 | for _, item := range output.Items {
59 | var certEntry dynamoCertEntry
60 | if err := A.UnmarshalMap(item, &certEntry); err != nil {
61 | log.WithError(err).Error("Error unmarshaling cert from Dynamo")
62 | continue
63 | }
64 |
65 | info, err := certEntry.toCertificateInfo()
66 | if err != nil {
67 | log.WithError(err).Error("Error parsing certificate from Dynamo")
68 | continue
69 | }
70 |
71 | certs = append(certs, info)
72 | }
73 |
74 | return b
75 | }); err != nil {
76 | return nil, err
77 | }
78 |
79 | return certs, nil
80 | }
81 |
82 | func (s *awsStorage) ListCertsBySubject(ctx context.Context, subjectName string) ([]*pki.CertificateInfo, error) {
83 | exp, err := E.NewBuilder().
84 | WithKeyCondition(E.KeyEqual(E.Key(kAttrSubjectName), E.Value(subjectName))).
85 | WithFilter(E.GreaterThan(E.Key(kAttrValidUntil), E.Value(time.Now().UTC().Unix()))).
86 | Build()
87 | if err != nil {
88 | return nil, err
89 | }
90 |
91 | query := dynamodb.QueryInput{
92 | TableName: aws.String(configAWSPKI.TableName),
93 | IndexName: aws.String(kIndexSubjectName),
94 |
95 | ExpressionAttributeNames: exp.Names(),
96 | ExpressionAttributeValues: exp.Values(),
97 | KeyConditionExpression: exp.KeyCondition(),
98 | FilterExpression: exp.Filter(),
99 |
100 | ScanIndexForward: aws.Bool(false),
101 | }
102 |
103 | certs := make([]*pki.CertificateInfo, 0)
104 | if err := s.ddb.QueryPagesWithContext(ctx, &query, func(output *dynamodb.QueryOutput, b bool) bool {
105 | for _, item := range output.Items {
106 | var certEntry dynamoCertEntry
107 | if err := A.UnmarshalMap(item, &certEntry); err != nil {
108 | log.WithError(err).Errorf("error unmarshaling cert")
109 | continue
110 | }
111 |
112 | info, err := certEntry.toCertificateInfo()
113 | if err != nil {
114 | log.WithError(err).Errorf("error parsing certificate")
115 | continue
116 | }
117 |
118 | certs = append(certs, info)
119 | }
120 |
121 | return b
122 | }); err != nil {
123 | return nil, err
124 | }
125 |
126 | return certs, nil
127 | }
128 |
129 | func (s *awsStorage) AddCert(ctx context.Context, cert *x509.Certificate) error {
130 | if cert.Raw == nil {
131 | return errors.New("missing cert raw data")
132 | }
133 |
134 | if cert.AuthorityKeyId == nil {
135 | return errors.New("missing Authority Key ID")
136 | }
137 |
138 | if cert.SubjectKeyId == nil {
139 | return errors.New("missing Subject Key ID")
140 | }
141 |
142 | cType := pki.GetCertType(cert)
143 | if cType == pki.CertTypeUnknown {
144 | return errors.New("unknown certificate type")
145 | }
146 |
147 | entry := dynamoCertEntry{
148 | SerialNumber: cert.SerialNumber.Bytes(),
149 | AuthorityKeyId: cert.AuthorityKeyId,
150 | SubjectKeyId: cert.SubjectKeyId,
151 | SubjectName: cert.Subject.CommonName,
152 | CertType: string(cType),
153 | IssuedAt: cert.NotBefore.UTC(),
154 | ValidUntil: cert.NotAfter.UTC(),
155 | RevocationTime: time.Unix(0, 0).UTC(),
156 | Data: cert.Raw,
157 | }
158 |
159 | item, err := A.MarshalMap(entry)
160 | if err != nil {
161 | return err
162 | }
163 |
164 | _, err = s.ddb.PutItemWithContext(ctx, &dynamodb.PutItemInput{
165 | TableName: aws.String(configAWSPKI.TableName),
166 | Item: item,
167 | })
168 |
169 | return err
170 | }
171 |
172 | func (s *awsStorage) RevokeCert(ctx context.Context, serial []byte) (*pki.CertificateInfo, error) {
173 | expr, err := E.NewBuilder().
174 | WithCondition(E.Equal(E.Name(kAttrRevocationTime), E.Value(0))).
175 | WithUpdate(E.Set(E.Name(kAttrRevocationTime), E.Value(time.Now().UTC().Unix()))).
176 | Build()
177 | if err != nil {
178 | return nil, err
179 | }
180 |
181 | res, err := s.ddb.UpdateItemWithContext(ctx, &dynamodb.UpdateItemInput{
182 | TableName: aws.String(configAWSPKI.TableName),
183 | Key: map[string]*dynamodb.AttributeValue{
184 | kAttrSerialNumber: {B: serial},
185 | },
186 |
187 | ExpressionAttributeNames: expr.Names(),
188 | ExpressionAttributeValues: expr.Values(),
189 | ConditionExpression: expr.Condition(),
190 | UpdateExpression: expr.Update(),
191 |
192 | ReturnValues: aws.String(dynamodb.ReturnValueAllNew),
193 | })
194 | if err != nil {
195 | return nil, err
196 | }
197 |
198 | var certEntry dynamoCertEntry
199 | if err := A.UnmarshalMap(res.Attributes, &certEntry); err != nil {
200 | return nil, err
201 | }
202 |
203 | return certEntry.toCertificateInfo()
204 | }
205 |
--------------------------------------------------------------------------------
/pkg/pki/aws/model.go:
--------------------------------------------------------------------------------
1 | package awspki
2 |
3 | import (
4 | "crypto/x509"
5 | "encoding/hex"
6 | "fmt"
7 | "strings"
8 | "time"
9 |
10 | "github.com/empathybroker/aws-vpn/pkg/pki"
11 | "github.com/pkg/errors"
12 | )
13 |
14 | const (
15 | kAttrSerialNumber = "SerialNumber"
16 | kAttrAuthorityKeyId = "AuthorityKeyId"
17 | kAttrSubjectKeyId = "SubjectKeyId"
18 | kAttrSubjectName = "SubjectName"
19 | kAttrCertType = "CertType"
20 | kAttrIssuedAt = "IssuedAt"
21 | kAttrValidUntil = "ValidUntil"
22 | kAttrRevocationTime = "RevocationTime"
23 | kAttrData = "Data"
24 |
25 | kIndexSubjectKeyId = kAttrSubjectKeyId + "Idx"
26 | kIndexSubjectName = kAttrSubjectName + "Idx"
27 | )
28 |
29 | type dynamoCertEntry struct {
30 | SerialNumber []byte `dynamodbav:",binary"`
31 | AuthorityKeyId []byte `dynamodbav:",binary"`
32 | SubjectKeyId []byte `dynamodbav:",binary"`
33 | SubjectName string `dynamodbav:",string"`
34 | CertType string `dynamodbav:",string"`
35 | IssuedAt time.Time `dynamodbav:",unixtime"`
36 | ValidUntil time.Time `dynamodbav:",unixtime"`
37 | RevocationTime time.Time `dynamodbav:",unixtime"`
38 | Data []byte `dynamodbav:",binary"`
39 | }
40 |
41 | func (e *dynamoCertEntry) toCertificateInfo() (*pki.CertificateInfo, error) {
42 | if e.Data == nil {
43 | return nil, nil
44 | }
45 |
46 | cert, err := x509.ParseCertificate(e.Data)
47 | if err != nil {
48 | return nil, errors.Wrap(err, "parsing certificate")
49 | }
50 |
51 | info := &pki.CertificateInfo{
52 | Certificate: cert,
53 | SerialBytes: e.SerialNumber,
54 |
55 | CertType: pki.CertType(e.CertType),
56 | Serial: hex.EncodeToString(e.SerialNumber),
57 | KeyId: hex.EncodeToString(e.SubjectKeyId),
58 | Subject: e.SubjectName,
59 | NotBefore: e.IssuedAt.UTC(),
60 | NotAfter: e.ValidUntil.UTC(),
61 | }
62 |
63 | if e.RevocationTime.Unix() > 0 {
64 | rt := e.RevocationTime.UTC()
65 | info.Revoked = &rt
66 | }
67 |
68 | return info, err
69 | }
70 |
71 | func (e dynamoCertEntry) String() string {
72 | vals := []string{
73 | fmt.Sprintf("SerialNumber:%s", hex.EncodeToString(e.SerialNumber)),
74 | fmt.Sprintf("AuthorityKeyId:%s", hex.EncodeToString(e.AuthorityKeyId)),
75 | fmt.Sprintf("SubjectKeyId:%s", hex.EncodeToString(e.SubjectKeyId)),
76 | fmt.Sprintf("SubjectName:%s", e.SubjectName),
77 | fmt.Sprintf("CertType:%s", e.CertType),
78 | fmt.Sprintf("IssuedAt:%s", e.IssuedAt),
79 | fmt.Sprintf("ValidUntil:%s", e.ValidUntil),
80 | fmt.Sprintf("RevocationTime:%s", e.RevocationTime),
81 | }
82 |
83 | return fmt.Sprintf("{%s}", strings.Join(vals, ", "))
84 | }
85 |
86 | type RevokedCert struct {
87 | SerialNumber []byte
88 | RevocationTime time.Time
89 | }
90 |
--------------------------------------------------------------------------------
/pkg/pki/aws/secrets.go:
--------------------------------------------------------------------------------
1 | package awspki
2 |
3 | import (
4 | "context"
5 | "crypto"
6 | "crypto/x509"
7 | "time"
8 |
9 | "github.com/aws/aws-sdk-go/aws"
10 | "github.com/aws/aws-sdk-go/service/secretsmanager"
11 | "github.com/empathybroker/aws-vpn/pkg/pki"
12 | log "github.com/sirupsen/logrus"
13 | )
14 |
15 | func (s *awsStorage) maybeUpdate(ctx context.Context) {
16 | if time.Now().After(s.exp) {
17 | log.Debug("Updating CA secrets")
18 | res, err := s.sm.GetSecretValueWithContext(ctx, &secretsmanager.GetSecretValueInput{
19 | SecretId: aws.String(configAWSPKI.SecretName),
20 | })
21 | if err != nil {
22 | log.WithError(err).Error("Fetching CA Key")
23 | return
24 | }
25 |
26 | if err := s.data.UnmarshalJSON(res.SecretBinary); err != nil {
27 | log.WithError(err).Error("Unmarshalling CA Key")
28 | return
29 | }
30 |
31 | s.exp = time.Now().Add(1 * time.Minute)
32 | }
33 | }
34 |
35 | func (s *awsStorage) GetCACert(ctx context.Context) *x509.Certificate {
36 | s.mut.Lock()
37 | defer s.mut.Unlock()
38 | s.maybeUpdate(ctx)
39 |
40 | return s.data.CACert
41 | }
42 |
43 | func (s *awsStorage) GetPrevCACert(ctx context.Context) *x509.Certificate {
44 | s.mut.Lock()
45 | defer s.mut.Unlock()
46 | s.maybeUpdate(ctx)
47 |
48 | return s.data.PrevCACert
49 | }
50 |
51 | func (s *awsStorage) GetCrossCert(ctx context.Context) *x509.Certificate {
52 | s.mut.Lock()
53 | defer s.mut.Unlock()
54 | s.maybeUpdate(ctx)
55 |
56 | return s.data.CrossCert
57 | }
58 |
59 | func (s *awsStorage) GetPrivateKey(ctx context.Context) crypto.PrivateKey {
60 | s.mut.Lock()
61 | defer s.mut.Unlock()
62 | s.maybeUpdate(ctx)
63 |
64 | return s.data.PrivateKey
65 | }
66 |
67 | func (s *awsStorage) GetPublicKey(ctx context.Context) crypto.PublicKey {
68 | s.mut.Lock()
69 | defer s.mut.Unlock()
70 | s.maybeUpdate(ctx)
71 |
72 | return s.data.PublicKey
73 | }
74 |
75 | func (s *awsStorage) GetStaticKey(ctx context.Context) pki.StaticKey {
76 | s.mut.Lock()
77 | defer s.mut.Unlock()
78 | s.maybeUpdate(ctx)
79 |
80 | return s.data.StaticKey
81 | }
82 |
--------------------------------------------------------------------------------
/pkg/pki/cakey.go:
--------------------------------------------------------------------------------
1 | package pki
2 |
3 | import (
4 | "crypto"
5 | "crypto/x509"
6 | "crypto/x509/pkix"
7 | "encoding/json"
8 | "time"
9 |
10 | "github.com/pkg/errors"
11 | jose "gopkg.in/square/go-jose.v2"
12 | )
13 |
14 | type CAData struct {
15 | PrivateKey crypto.PrivateKey
16 | PublicKey crypto.PublicKey
17 |
18 | CACert *x509.Certificate
19 | PrevCACert *x509.Certificate
20 | CrossCert *x509.Certificate
21 |
22 | StaticKey StaticKey
23 | }
24 |
25 | type storedCAData struct {
26 | PrivateKey jose.JSONWebKey `json:"key"`
27 |
28 | CACert []byte `json:"ca"`
29 | PrevCACert []byte `json:"pca,omitempty"`
30 | CrossCert []byte `json:"xca,omitempty"`
31 | StaticKey []byte `json:"ovpn"`
32 | }
33 |
34 | func (k *CAData) UnmarshalJSON(data []byte) error {
35 | var stored storedCAData
36 | if err := json.Unmarshal(data, &stored); err != nil {
37 | return errors.Wrap(err, "unmarshal CA data")
38 | }
39 |
40 | parsed, err := x509.ParseCertificate(stored.CACert)
41 | if err != nil {
42 | return errors.Wrap(err, "parsing CA certificate")
43 | }
44 | k.CACert = parsed
45 |
46 | if len(stored.PrevCACert) > 0 {
47 | k.PrevCACert, err = x509.ParseCertificate(stored.PrevCACert)
48 | if err != nil {
49 | return errors.Wrap(err, "parsing old CA certificate")
50 | }
51 | }
52 |
53 | if len(stored.CrossCert) > 0 {
54 | k.CrossCert, err = x509.ParseCertificate(stored.CrossCert)
55 | if err != nil {
56 | return errors.Wrap(err, "parsing cross-signed CA certificate")
57 | }
58 | }
59 |
60 | var ok bool
61 | if k.PrivateKey, ok = stored.PrivateKey.Key.(crypto.PrivateKey); !ok {
62 | return errors.New("unexpected privateKey type")
63 | }
64 |
65 | if k.PublicKey, ok = stored.PrivateKey.Public().Key.(crypto.PublicKey); !ok {
66 | return errors.New("unexpected publicKey type")
67 | }
68 |
69 | k.StaticKey = stored.StaticKey
70 |
71 | return nil
72 | }
73 |
74 | func (k CAData) MarshalJSON() ([]byte, error) {
75 | s := storedCAData{
76 | PrivateKey: jose.JSONWebKey{Key: k.PrivateKey},
77 | CACert: k.CACert.Raw,
78 | StaticKey: k.StaticKey,
79 | }
80 |
81 | if k.PrevCACert != nil {
82 | s.PrevCACert = k.PrevCACert.Raw
83 | }
84 |
85 | if k.CrossCert != nil {
86 | s.CrossCert = k.CrossCert.Raw
87 | }
88 |
89 | return json.Marshal(s)
90 | }
91 |
92 | func NewCAKey(caName string, serialNumber string, duration time.Duration) (CAData, error) {
93 | privKey, err := NewPrivateKey()
94 | if err != nil {
95 | return CAData{}, errors.Wrap(err, "error generating key")
96 | }
97 |
98 | pkiName := pkix.Name{CommonName: caName, SerialNumber: serialNumber}
99 | caCert, err := CreateCertificate(nil, privKey, GetPublicKey(privKey), pkiName, CACert, WithDuration(duration), WithMaxPathLen(1))
100 | if err != nil {
101 | return CAData{}, errors.Wrap(err, "error signing certificate key")
102 | }
103 |
104 | return CAData{
105 | PrivateKey: privKey,
106 | PublicKey: GetPublicKey(privKey),
107 | CACert: caCert,
108 | PrevCACert: nil,
109 | CrossCert: nil,
110 | StaticKey: NewStaticKey(),
111 | }, nil
112 | }
113 |
114 | func (k CAData) Renew(caName string, serialNumber string, duration time.Duration) (CAData, error) {
115 | privKey, err := NewPrivateKey()
116 | if err != nil {
117 | return CAData{}, errors.Wrap(err, "error generating key")
118 | }
119 |
120 | pkiName := pkix.Name{CommonName: caName, SerialNumber: serialNumber}
121 | caCert, err := CreateCertificate(nil, privKey, GetPublicKey(privKey), pkiName, CACert, WithDuration(duration), WithMaxPathLen(1))
122 | if err != nil {
123 | return CAData{}, errors.Wrap(err, "error signing CA certificate")
124 | }
125 |
126 | crossCert, err := CreateCertificate(k.CACert, k.PrivateKey, GetPublicKey(privKey), pkiName, CACert, WithExpiration(k.CACert.NotAfter), WithMaxPathLen(0))
127 | if err != nil {
128 | return CAData{}, errors.Wrap(err, "error cross-signing CA certificate")
129 | }
130 |
131 | return CAData{
132 | PrivateKey: privKey,
133 | PublicKey: GetPublicKey(privKey),
134 | CACert: caCert,
135 | PrevCACert: k.CACert,
136 | CrossCert: crossCert,
137 | StaticKey: k.StaticKey,
138 | }, nil
139 | }
140 |
--------------------------------------------------------------------------------
/pkg/pki/cakey_test.go:
--------------------------------------------------------------------------------
1 | package pki
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 | "testing"
7 | "time"
8 |
9 | "github.com/google/uuid"
10 | "github.com/sirupsen/logrus"
11 | )
12 |
13 | func init() {
14 | logrus.SetLevel(logrus.DebugLevel)
15 |
16 | if err := os.Setenv("PKI_KEY_TYPE", "RSA"); err != nil {
17 | panic(err)
18 | }
19 | }
20 |
21 | const (
22 | kCAName = "EmpathyBroker VPN"
23 | kDuration = 90 * 24 * time.Hour
24 | )
25 |
26 | func TestNewCAKey(t *testing.T) {
27 | if err := os.Setenv("PKI_KEY_TYPE", "RSA"); err != nil {
28 | t.Fatal(err)
29 | }
30 |
31 | caKey, err := NewCAKey(kCAName, uuid.New().String(), kDuration)
32 | if err != nil {
33 | t.Fatalf("%+v", err)
34 | }
35 |
36 | rCaKey, err := caKey.Renew(kCAName, uuid.New().String(), kDuration)
37 | if err != nil {
38 | t.Fatal(err)
39 | }
40 |
41 | caData, err := json.Marshal(caKey)
42 | if err != nil {
43 | t.Fatal(err)
44 | }
45 |
46 | t.Log(len(caData), string(caData))
47 |
48 | rCaData, err := json.Marshal(rCaKey)
49 | if err != nil {
50 | t.Fatal(err)
51 | }
52 |
53 | t.Log(len(rCaData), string(rCaData))
54 | }
55 |
56 | func TestRenewSize(t *testing.T) {
57 | maxSize := 7 * 1024
58 |
59 | max := []int{0, 0, 0}
60 |
61 | for i := 0; i < 10; i++ {
62 | caKey, err := NewCAKey(kCAName, uuid.New().String(), kDuration)
63 | if err != nil {
64 | t.Fatal(err)
65 | }
66 |
67 | caData, err := json.Marshal(caKey)
68 | if err != nil {
69 | t.Fatal(err)
70 | }
71 |
72 | if len(caData) > max[0] {
73 | max[0] = len(caData)
74 | }
75 | // t.Log(0, len(caData))
76 |
77 | caKey, err = caKey.Renew(kCAName, uuid.New().String(), kDuration)
78 | if err != nil {
79 | t.Fatal(err)
80 | }
81 |
82 | caData, err = json.Marshal(caKey)
83 | if err != nil {
84 | t.Fatal(err)
85 | }
86 |
87 | if len(caData) > max[1] {
88 | max[1] = len(caData)
89 | }
90 | t.Log(1, len(caData), string(caData))
91 |
92 | if len(caData) > maxSize {
93 | t.Fatalf("%d > %d", len(caData), maxSize)
94 | }
95 | }
96 |
97 | t.Log(max)
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/pkg/pki/certs.go:
--------------------------------------------------------------------------------
1 | package pki
2 |
3 | import (
4 | "crypto"
5 | "crypto/rand"
6 | "crypto/x509"
7 | "crypto/x509/pkix"
8 | )
9 |
10 | func CreateCertificate(parent *x509.Certificate, privKey crypto.PrivateKey, pubKey crypto.PublicKey, subject pkix.Name, certOpts ...CertOptions) (*x509.Certificate, error) {
11 | ski, err := getSKI(pubKey)
12 | if err != nil {
13 | return nil, err
14 | }
15 |
16 | template := &x509.Certificate{
17 | Subject: subject,
18 | SerialNumber: randomSerial(),
19 | PublicKey: pubKey,
20 | SubjectKeyId: ski,
21 | }
22 |
23 | for _, opt := range certOpts {
24 | opt(template)
25 | }
26 |
27 | if parent == nil {
28 | parent = template
29 | }
30 |
31 | certData, err := x509.CreateCertificate(rand.Reader, template, parent, pubKey, privKey)
32 | if err != nil {
33 | return nil, err
34 | }
35 |
36 | return x509.ParseCertificate(certData)
37 | }
38 |
--------------------------------------------------------------------------------
/pkg/pki/model.go:
--------------------------------------------------------------------------------
1 | package pki
2 |
3 | import (
4 | "context"
5 | "crypto"
6 | "crypto/x509"
7 | "encoding/hex"
8 | "time"
9 | )
10 |
11 | type CertificateInfo struct {
12 | Certificate *x509.Certificate `json:"-"`
13 | SerialBytes []byte `json:"-"`
14 |
15 | CertType CertType `json:"type"`
16 | Serial string `json:"serial"`
17 | KeyId string `json:"keyId"`
18 | Subject string `json:"subject"`
19 | NotBefore time.Time `json:"notBefore"`
20 | NotAfter time.Time `json:"notAfter"`
21 | Revoked *time.Time `json:"revoked,omitempty"`
22 | }
23 |
24 | func CertInfoFromX509Cert(cert *x509.Certificate) *CertificateInfo {
25 | return &CertificateInfo{
26 | Certificate: cert,
27 | SerialBytes: cert.SerialNumber.Bytes(),
28 |
29 | CertType: GetCertType(cert),
30 | Serial: hex.EncodeToString(cert.SerialNumber.Bytes()),
31 | KeyId: hex.EncodeToString(cert.SubjectKeyId),
32 | Subject: cert.Subject.CommonName,
33 | NotBefore: cert.NotBefore,
34 | NotAfter: cert.NotAfter,
35 | Revoked: nil,
36 | }
37 | }
38 |
39 | type PKIStorage interface {
40 | GetCACert(ctx context.Context) *x509.Certificate
41 | GetPrevCACert(ctx context.Context) *x509.Certificate
42 | GetCrossCert(ctx context.Context) *x509.Certificate
43 | GetPrivateKey(ctx context.Context) crypto.PrivateKey
44 | GetPublicKey(ctx context.Context) crypto.PublicKey
45 | GetStaticKey(ctx context.Context) StaticKey
46 |
47 | AddCert(ctx context.Context, cert *x509.Certificate) error
48 | ListAllCerts(ctx context.Context) ([]*CertificateInfo, error)
49 | ListCertsBySubject(context.Context, string) ([]*CertificateInfo, error)
50 | GetCertBySerial(context.Context, []byte) (*CertificateInfo, error)
51 | RevokeCert(context.Context, []byte) (*CertificateInfo, error)
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/pki/options.go:
--------------------------------------------------------------------------------
1 | package pki
2 |
3 | import (
4 | "crypto/x509"
5 | "time"
6 | )
7 |
8 | type CertOptions func(cert *x509.Certificate)
9 |
10 | func CACert(cert *x509.Certificate) {
11 | cert.IsCA = true
12 | cert.BasicConstraintsValid = true
13 | cert.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign
14 | cert.ExtKeyUsage = nil
15 | }
16 |
17 | func ClientCert(cert *x509.Certificate) {
18 | cert.IsCA = false
19 | cert.BasicConstraintsValid = true
20 | cert.KeyUsage = x509.KeyUsageDigitalSignature
21 | cert.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
22 | }
23 |
24 | func ServerCert(cert *x509.Certificate) {
25 | cert.IsCA = false
26 | cert.BasicConstraintsValid = true
27 | cert.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
28 | cert.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
29 | }
30 |
31 | func WithTimespan(notBefore time.Time, notAfter time.Time) CertOptions {
32 | return func(cert *x509.Certificate) {
33 | cert.NotBefore = notBefore
34 | cert.NotAfter = notAfter
35 | }
36 | }
37 |
38 | func WithDuration(duration time.Duration) CertOptions {
39 | notBefore := time.Now()
40 | return WithTimespan(notBefore, notBefore.Add(duration))
41 | }
42 |
43 | func WithExpiration(notAfter time.Time) CertOptions {
44 | return WithTimespan(time.Now(), notAfter)
45 | }
46 |
47 | func WithMaxPathLen(pathLen int) CertOptions {
48 | return func(cert *x509.Certificate) {
49 | cert.MaxPathLen = pathLen
50 | cert.MaxPathLenZero = pathLen == 0
51 | }
52 | }
53 |
54 | func WithEmail(email ...string) CertOptions {
55 | return func(cert *x509.Certificate) {
56 | cert.EmailAddresses = email
57 | }
58 | }
59 |
60 | func WithDNS(dnsName ...string) CertOptions {
61 | return func(cert *x509.Certificate) {
62 | cert.DNSNames = dnsName
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/pki/pki.go:
--------------------------------------------------------------------------------
1 | package pki
2 |
3 | import (
4 | "context"
5 | "crypto"
6 | "crypto/x509"
7 | "crypto/x509/pkix"
8 |
9 | "github.com/pkg/errors"
10 | )
11 |
12 | type PKI struct {
13 | storage PKIStorage
14 | }
15 |
16 | func NewPKI(s PKIStorage) *PKI {
17 | return &PKI{
18 | storage: s,
19 | }
20 | }
21 |
22 | func (pki *PKI) GetCACert(ctx context.Context) *x509.Certificate {
23 | return pki.storage.GetCACert(ctx)
24 | }
25 |
26 | func (pki *PKI) GetPrevCACert(ctx context.Context) *x509.Certificate {
27 | return pki.storage.GetPrevCACert(ctx)
28 | }
29 |
30 | func (pki *PKI) GetCrossCert(ctx context.Context) *x509.Certificate {
31 | return pki.storage.GetCrossCert(ctx)
32 | }
33 |
34 | func (pki *PKI) GetStaticKey(ctx context.Context) StaticKey {
35 | return pki.storage.GetStaticKey(ctx)
36 | }
37 |
38 | func (pki *PKI) CreateCertificate(ctx context.Context, pubKey crypto.PublicKey, subject pkix.Name, certOpts ...CertOptions) (*CertificateInfo, error) {
39 | cert, err := CreateCertificate(pki.storage.GetCACert(ctx), pki.storage.GetPrivateKey(ctx), pubKey, subject, certOpts...)
40 | if err != nil {
41 | return nil, errors.Wrap(err, "creating certificate")
42 | }
43 |
44 | if err := pki.storage.AddCert(ctx, cert); err != nil {
45 | return nil, errors.Wrap(err, "storing certificate")
46 | }
47 |
48 | return CertInfoFromX509Cert(cert), nil
49 | }
50 |
51 | func (pki *PKI) GetCertBySerial(ctx context.Context, serial []byte) (*CertificateInfo, error) {
52 | return pki.storage.GetCertBySerial(ctx, serial)
53 | }
54 |
55 | func (pki *PKI) ListCerts(ctx context.Context, subject string) ([]*CertificateInfo, error) {
56 | if subject == "" {
57 | return pki.storage.ListAllCerts(ctx)
58 | }
59 | return pki.storage.ListCertsBySubject(ctx, subject)
60 | }
61 |
62 | func (pki *PKI) RevokeCert(ctx context.Context, serial []byte) (*CertificateInfo, error) {
63 | return pki.storage.RevokeCert(ctx, serial)
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/pki/statickey.go:
--------------------------------------------------------------------------------
1 | package pki
2 |
3 | import (
4 | "bytes"
5 | "crypto/rand"
6 | "encoding/hex"
7 | "fmt"
8 | )
9 |
10 | const (
11 | kStaticKeyBits = 2048
12 | )
13 |
14 | type StaticKey []byte
15 |
16 | func NewStaticKey() StaticKey {
17 | buf := make([]byte, kStaticKeyBits/8)
18 | if _, err := rand.Read(buf); err != nil {
19 | panic(err)
20 | }
21 | return StaticKey(buf)
22 | }
23 |
24 | func (k StaticKey) String() string {
25 | var buf bytes.Buffer
26 |
27 | buf.WriteString(fmt.Sprintf("#\n# %d bit OpenVPN static key\n#\n", 8*len(k)))
28 | buf.WriteString("-----BEGIN OpenVPN Static key V1-----\n")
29 |
30 | hexEnc := hex.NewEncoder(&buf)
31 | for i := 0; i < len(k); i += 16 {
32 | if _, err := hexEnc.Write(k[i : i+16]); err != nil {
33 | panic(err)
34 | }
35 | buf.WriteByte('\n')
36 | }
37 |
38 | buf.WriteString("-----END OpenVPN Static key V1-----\n")
39 |
40 | return buf.String()
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/pki/utils.go:
--------------------------------------------------------------------------------
1 | package pki
2 |
3 | import (
4 | "crypto"
5 | "crypto/ecdsa"
6 | "crypto/elliptic"
7 | "crypto/rand"
8 | "crypto/rsa"
9 | "crypto/sha1"
10 | "crypto/x509"
11 | "crypto/x509/pkix"
12 | "encoding/asn1"
13 | "encoding/hex"
14 | "encoding/pem"
15 | "math/big"
16 | "os"
17 |
18 | "github.com/pkg/errors"
19 | log "github.com/sirupsen/logrus"
20 | )
21 |
22 | type CertSerial *big.Int
23 | type CertType string
24 |
25 | const (
26 | CertTypeUnknown CertType = "UNKNOWN"
27 | CertTypeServer = "Server"
28 | CertTypeClient = "Client"
29 | CertTypeCA = "CA"
30 | )
31 |
32 | var kSerialMaxValue = new(big.Int).Lsh(big.NewInt(1), 128)
33 |
34 | func DecodeSerial(serial string) ([]byte, error) {
35 | decoded, err := hex.DecodeString(serial)
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | res := big.NewInt(0).SetBytes(decoded)
41 | if res.Cmp(kSerialMaxValue) > 0 {
42 | return nil, errors.New("serial out of range")
43 | }
44 |
45 | return res.Bytes(), nil
46 | }
47 |
48 | func randomSerial() CertSerial {
49 | serial, err := rand.Int(rand.Reader, kSerialMaxValue)
50 | if err != nil {
51 | log.WithError(err).Fatal("failed to generate serial number")
52 | }
53 | return serial
54 | }
55 |
56 | func getSKI(key crypto.PublicKey) ([]byte, error) {
57 | keyASN, err := x509.MarshalPKIXPublicKey(key)
58 | var spki struct {
59 | Algo pkix.AlgorithmIdentifier
60 | BitString asn1.BitString
61 | }
62 |
63 | if rest, err := asn1.Unmarshal(keyASN, &spki); err != nil {
64 | return nil, errors.WithStack(err)
65 | } else if len(rest) > 0 {
66 | return nil, errors.Errorf("unexpected %d remaining bytes", len(rest))
67 | }
68 |
69 | h := sha1.Sum(spki.BitString.Bytes)
70 | return h[:], err
71 | }
72 |
73 | func EncodePEMCert(cert *x509.Certificate) []byte {
74 | return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
75 | }
76 |
77 | func EncodeDERPrivateKey(key crypto.PrivateKey) ([]byte, error) {
78 | switch key := key.(type) {
79 | case *rsa.PrivateKey:
80 | return x509.MarshalPKCS1PrivateKey(key), nil
81 | case *ecdsa.PrivateKey:
82 | return x509.MarshalECPrivateKey(key)
83 | default:
84 | return nil, errors.New("unsupported key type")
85 | }
86 | }
87 |
88 | func EncodePEMPrivateKey(key crypto.PrivateKey) ([]byte, error) {
89 | derBytes, err := EncodeDERPrivateKey(key)
90 | if err != nil {
91 | return nil, errors.Wrap(err, "encoding private key")
92 | }
93 |
94 | var block pem.Block
95 | switch key.(type) {
96 | case *rsa.PrivateKey:
97 | block = pem.Block{Type: "RSA PRIVATE KEY", Bytes: derBytes}
98 | case *ecdsa.PrivateKey:
99 | block = pem.Block{Type: "EC PRIVATE KEY", Bytes: derBytes}
100 | default:
101 | return nil, errors.New("unsupported key type")
102 | }
103 |
104 | return pem.EncodeToMemory(&block), nil
105 | }
106 |
107 | func NewPrivateKey() (crypto.PrivateKey, error) {
108 | switch os.Getenv("PKI_KEY_TYPE") {
109 | case "RSA":
110 | return rsa.GenerateKey(rand.Reader, 2048)
111 | case "EC":
112 | return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
113 | default:
114 | return nil, errors.New("invalid PKI_KEY_TYPE")
115 | }
116 | }
117 |
118 | func GetPublicKey(key crypto.PrivateKey) crypto.PublicKey {
119 | switch key := key.(type) {
120 | case *rsa.PrivateKey:
121 | return key.Public()
122 | case *ecdsa.PrivateKey:
123 | return key.Public()
124 | default:
125 | panic("unsupported key type")
126 | }
127 | }
128 |
129 | func GetCertType(cert *x509.Certificate) CertType {
130 | if cert.IsCA {
131 | return CertTypeCA
132 | }
133 |
134 | for _, eku := range cert.ExtKeyUsage {
135 | if eku == x509.ExtKeyUsageServerAuth {
136 | return CertTypeServer
137 | }
138 | if eku == x509.ExtKeyUsageClientAuth {
139 | return CertTypeClient
140 | }
141 | }
142 |
143 | return CertTypeUnknown
144 | }
145 |
--------------------------------------------------------------------------------