Skip to content

Commit 312f8d1

Browse files
arekkasarekkas
authored andcommitted
cmd: Allows import of PEM/DER/JSON encoded keys
Closes #98 Signed-off-by: arekkas <aeneas@ory.am>
1 parent 3bbd5e8 commit 312f8d1

14 files changed

+326
-14
lines changed

cmd/cli/handler_client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func newClientHandler(c *config.Config) *ClientHandler {
4545
}
4646

4747
func (h *ClientHandler) newClientManager(cmd *cobra.Command) *hydra.OAuth2Api {
48-
c := hydra.NewOAuth2ApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlash(cmd))
48+
c := hydra.NewOAuth2ApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlashOrFail(cmd))
4949

5050
fakeTlsTermination, _ := cmd.Flags().GetBool("skip-tls-verify")
5151
c.Configuration.Transport = &http.Transport{

cmd/cli/handler_helper.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,14 @@ import (
3030
)
3131

3232
func checkResponse(response *hydra.APIResponse, err error, expectedStatusCode int) {
33-
pkg.Must(err, "Command failed because error \"%s\" occurred.\n", err)
33+
if response != nil {
34+
pkg.Must(err, "Command failed because calling \"%s %s\" resulted in error \"%s\" occurred.\n%s\n", response.Request.Method, response.RequestURL, err, response.Payload)
35+
} else {
36+
pkg.Must(err, "Command failed because error \"%s\" occurred and no response is available.\n", err)
37+
}
3438

3539
if response.StatusCode != expectedStatusCode {
36-
fmt.Fprintf(os.Stderr, "Command failed because status code %d was expeceted but code %d was received.\n", expectedStatusCode, response.StatusCode)
37-
fmt.Fprintf(os.Stderr, "The server responded with:\n%s\n", response.Payload)
40+
fmt.Fprintf(os.Stderr, "Command failed because calling \"%s %s\" resulted in status code \"%d\" but code \"%d\" was expected.\n%s\n", response.Request.Method, response.RequestURL, expectedStatusCode, response.StatusCode, response.Payload)
3841
os.Exit(1)
3942
return
4043
}

cmd/cli/handler_introspection.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func (h *IntrospectionHandler) Introspect(cmd *cobra.Command, args []string) {
5151
return
5252
}
5353

54-
c := hydra.NewOAuth2ApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlash(cmd))
54+
c := hydra.NewOAuth2ApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlashOrFail(cmd))
5555

5656
clientID, _ := cmd.Flags().GetString("client-id")
5757
clientSecret, _ := cmd.Flags().GetString("client-secret")

cmd/cli/handler_jwk.go

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,29 @@
2121
package cli
2222

2323
import (
24+
"bytes"
2425
"crypto/tls"
26+
"encoding/json"
2527
"fmt"
28+
"io/ioutil"
2629
"net/http"
30+
"os"
2731

32+
"github.com/mendsley/gojwk"
2833
"github.com/ory/hydra/config"
34+
"github.com/ory/hydra/pkg"
2935
hydra "github.com/ory/hydra/sdk/go/hydra/swagger"
36+
"github.com/pborman/uuid"
3037
"github.com/spf13/cobra"
38+
"gopkg.in/square/go-jose.v2"
3139
)
3240

3341
type JWKHandler struct {
3442
Config *config.Config
3543
}
3644

3745
func (h *JWKHandler) newJwkManager(cmd *cobra.Command) *hydra.JsonWebKeyApi {
38-
c := hydra.NewJsonWebKeyApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlash(cmd))
46+
c := hydra.NewJsonWebKeyApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlashOrFail(cmd))
3947

4048
skipTLSTermination, _ := cmd.Flags().GetBool("skip-tls-verify")
4149
c.Configuration.Transport = &http.Transport{
@@ -76,6 +84,114 @@ func (h *JWKHandler) CreateKeys(cmd *cobra.Command, args []string) {
7684
fmt.Printf("%s\n", formatResponse(keys))
7785
}
7886

87+
func toSDKFriendlyJSONWebKey(key interface{}, kid string, use string, public bool) jose.JSONWebKey {
88+
if jwk, ok := key.(*jose.JSONWebKey); ok {
89+
key = jwk.Key
90+
if jwk.KeyID != "" {
91+
kid = jwk.KeyID
92+
}
93+
if jwk.Use != "" {
94+
use = jwk.Use
95+
}
96+
}
97+
98+
var err error
99+
var jwk *gojwk.Key
100+
if public {
101+
jwk, err = gojwk.PublicKey(key)
102+
pkg.Must(err, "Unable to convert public key to JSON Web Key because %s", err)
103+
} else {
104+
jwk, err = gojwk.PrivateKey(key)
105+
pkg.Must(err, "Unable to convert private key to JSON Web Key because %s", err)
106+
}
107+
108+
return jose.JSONWebKey{
109+
KeyID: kid,
110+
Use: use,
111+
Algorithm: jwk.Alg,
112+
Key: key,
113+
}
114+
}
115+
116+
func (h *JWKHandler) ImportKeys(cmd *cobra.Command, args []string) {
117+
if len(args) < 2 {
118+
fmt.Println(cmd.UsageString())
119+
return
120+
}
121+
122+
id := args[0]
123+
use, _ := cmd.Flags().GetString("use")
124+
client := &http.Client{}
125+
126+
if skipTLSTermination, _ := cmd.Flags().GetBool("skip-tls-verify"); skipTLSTermination {
127+
client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTLSTermination}}
128+
}
129+
130+
u := h.Config.GetClusterURLWithoutTailingSlashOrFail(cmd) + "/keys/" + id
131+
request, err := http.NewRequest("GET", u, nil)
132+
pkg.Must(err, "Unable to initialize HTTP request")
133+
134+
if term, _ := cmd.Flags().GetBool("fake-tls-termination"); term {
135+
request.Header.Set("X-Forwarded-Proto", "https")
136+
}
137+
138+
if token, _ := cmd.Flags().GetString("access-token"); token != "" {
139+
request.Header.Set("Authorization", "Bearer "+token)
140+
}
141+
142+
response, err := client.Do(request)
143+
pkg.Must(err, "Unable to fetch data from %s because %s", u, err)
144+
defer response.Body.Close()
145+
146+
if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNotFound {
147+
fmt.Printf("Expected status code 200 or 404 but got %d while fetching data from %s.\n", response.StatusCode, u)
148+
os.Exit(1)
149+
}
150+
151+
var set jose.JSONWebKeySet
152+
pkg.Must(json.NewDecoder(response.Body).Decode(&set), "Unable to decode payload to JSON")
153+
154+
for _, path := range args[1:] {
155+
file, err := ioutil.ReadFile(path)
156+
pkg.Must(err, "Unable to read file %s", path)
157+
158+
if key, privateErr := pkg.LoadPrivateKey(file); privateErr != nil {
159+
key, publicErr := pkg.LoadPublicKey(file)
160+
if publicErr != nil {
161+
fmt.Printf("Unable to read key from file %s. Decoding file to private key failed with reason \"%s\" and decoding it to public key failed with reason \"%s\".\n", path, privateErr, publicErr)
162+
os.Exit(1)
163+
}
164+
165+
set.Keys = append(set.Keys, toSDKFriendlyJSONWebKey(key, "public:"+uuid.New(), use, true))
166+
} else {
167+
set.Keys = append(set.Keys, toSDKFriendlyJSONWebKey(key, "private:"+uuid.New(), use, false))
168+
}
169+
170+
fmt.Printf("Successfully loaded key from file %s\n", path)
171+
}
172+
173+
body, err := json.Marshal(&set)
174+
pkg.Must(err, "Unable to encode JSON Web Keys to JSON")
175+
176+
request, err = http.NewRequest("PUT", u, bytes.NewReader(body))
177+
pkg.Must(err, "Unable to initialize HTTP request")
178+
179+
if term, _ := cmd.Flags().GetBool("fake-tls-termination"); term {
180+
request.Header.Set("X-Forwarded-Proto", "https")
181+
}
182+
183+
if token, _ := cmd.Flags().GetString("access-token"); token != "" {
184+
request.Header.Set("Authorization", "Bearer "+token)
185+
}
186+
request.Header.Set("Content-Type", "application/json")
187+
188+
response, err = client.Do(request)
189+
pkg.Must(err, "Unable to post data to %s because %s", u, err)
190+
defer response.Body.Close()
191+
192+
fmt.Println("Keys successfully imported!")
193+
}
194+
79195
func (h *JWKHandler) GetKeys(cmd *cobra.Command, args []string) {
80196
m := h.newJwkManager(cmd)
81197
if len(args) != 1 {

cmd/cli/handler_token.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type TokenHandler struct {
3636
}
3737

3838
func (h *TokenHandler) newTokenManager(cmd *cobra.Command) *hydra.OAuth2Api {
39-
c := hydra.NewOAuth2ApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlash(cmd))
39+
c := hydra.NewOAuth2ApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlashOrFail(cmd))
4040

4141
skipTLSTermination, _ := cmd.Flags().GetBool("skip-tls-verify")
4242
c.Configuration.Transport = &http.Transport{
@@ -64,7 +64,7 @@ func (h *TokenHandler) RevokeToken(cmd *cobra.Command, args []string) {
6464
return
6565
}
6666

67-
handler := hydra.NewOAuth2ApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlash(cmd))
67+
handler := hydra.NewOAuth2ApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlashOrFail(cmd))
6868

6969
skipTLSTermination, _ := cmd.Flags().GetBool("skip-tls-verify")
7070
handler.Configuration.Transport = &http.Transport{

cmd/keys_import.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright © 2018 NAME HERE <EMAIL ADDRESS>
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"github.com/spf13/cobra"
19+
)
20+
21+
// keysImportCmd represents the import command
22+
var keysImportCmd = &cobra.Command{
23+
Use: "import <set> <file-1> [<file-2> [<file-3 [<...>]]]",
24+
Short: "Imports cryptographic keys of any format to the JSON Web Key Store",
25+
Long: `This command allows you to import cryptographic keys to the JSON Web Key Store.
26+
27+
Currently supported formats are raw JSON Web Keys or PEM/DER encoded data. If the JSON Web Key Set exists already,
28+
the imported keys will be added to that set. Otherwise, a new set will be created.
29+
30+
Please be aware that importing a private key does not automatically import its public key as well.
31+
32+
Examples:
33+
hydra keys import my-set ./path/to/jwk.json ./path/to/jwk-2.json
34+
hydra keys import my-set ./path/to/rsa.key ./path/to/rsa.pub
35+
`,
36+
Run: cmdHandler.Keys.ImportKeys,
37+
}
38+
39+
func init() {
40+
keysCmd.AddCommand(keysImportCmd)
41+
keysImportCmd.Flags().String("use", "sig", "Sets the \"use\" value of the JSON Web Key if not \"use\" value was defined by the key itself")
42+
}

cmd/root_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ func TestExecute(t *testing.T) {
8787
{args: []string{"keys", "rotate", "--endpoint", endpoint, "foo"}},
8888
{args: []string{"keys", "get", "--endpoint", endpoint, "foo"}},
8989
{args: []string{"keys", "delete", "--endpoint", endpoint, "foo"}},
90+
{args: []string{"keys", "import", "--endpoint", endpoint, "import-1", "../test/stub/ecdh.key", "../test/stub/ecdh.pub"}},
91+
{args: []string{"keys", "import", "--endpoint", endpoint, "import-2", "../test/stub/rsa.key", "../test/stub/rsa.pub"}},
9092
{args: []string{"token", "revoke", "--endpoint", endpoint, "--client-secret", "foobar", "--client-id", "foobarbaz", "foo"}},
9193
{args: []string{"token", "client", "--endpoint", endpoint, "--client-secret", "foobar", "--client-id", "foobarbaz"}},
9294
{args: []string{"help", "migrate", "sql"}},

cmd/token_client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ var tokenClientCmd = &cobra.Command{
7676

7777
scopes, _ := cmd.Flags().GetStringSlice("scope")
7878

79-
cu, err := url.Parse(c.GetClusterURLWithoutTailingSlash(cmd))
80-
pkg.Must(err, `Unable to parse cluster url ("%s"): %s`, c.GetClusterURLWithoutTailingSlash(cmd), err)
79+
cu, err := url.Parse(c.GetClusterURLWithoutTailingSlashOrFail(cmd))
80+
pkg.Must(err, `Unable to parse cluster url ("%s"): %s`, c.GetClusterURLWithoutTailingSlashOrFail(cmd), err)
8181

8282
clientID, _ := cmd.Flags().GetString("client-id")
8383
clientSecret, _ := cmd.Flags().GetString("client-secret")

cmd/token_user.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@ var tokenUserCmd = &cobra.Command{
7676
}
7777

7878
if backend == "" {
79-
bu, err := url.Parse(c.GetClusterURLWithoutTailingSlash(cmd))
80-
pkg.Must(err, `Unable to parse cluster url ("%s"): %s`, c.GetClusterURLWithoutTailingSlash(cmd), err)
79+
bu, err := url.Parse(c.GetClusterURLWithoutTailingSlashOrFail(cmd))
80+
pkg.Must(err, `Unable to parse cluster url ("%s"): %s`, c.GetClusterURLWithoutTailingSlashOrFail(cmd), err)
8181
backend = urlx.AppendPaths(bu, "/oauth2/token").String()
8282
}
8383
if frontend == "" {
84-
fu, err := url.Parse(c.GetClusterURLWithoutTailingSlash(cmd))
85-
pkg.Must(err, `Unable to parse cluster url ("%s"): %s`, c.GetClusterURLWithoutTailingSlash(cmd), err)
84+
fu, err := url.Parse(c.GetClusterURLWithoutTailingSlashOrFail(cmd))
85+
pkg.Must(err, `Unable to parse cluster url ("%s"): %s`, c.GetClusterURLWithoutTailingSlashOrFail(cmd), err)
8686
frontend = urlx.AppendPaths(fu, "/oauth2/auth").String()
8787
}
8888

pkg/jose-utils.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*-
2+
* Copyright 2014 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package pkg
18+
19+
import (
20+
"crypto/x509"
21+
"encoding/pem"
22+
"errors"
23+
"fmt"
24+
25+
"gopkg.in/square/go-jose.v2"
26+
)
27+
28+
func LoadJSONWebKey(json []byte, pub bool) (*jose.JSONWebKey, error) {
29+
var jwk jose.JSONWebKey
30+
err := jwk.UnmarshalJSON(json)
31+
if err != nil {
32+
return nil, err
33+
}
34+
if !jwk.Valid() {
35+
return nil, errors.New("invalid JWK key")
36+
}
37+
if jwk.IsPublic() != pub {
38+
return nil, errors.New("priv/pub JWK key mismatch")
39+
}
40+
return &jwk, nil
41+
}
42+
43+
// LoadPublicKey loads a public key from PEM/DER/JWK-encoded data.
44+
func LoadPublicKey(data []byte) (interface{}, error) {
45+
input := data
46+
47+
block, _ := pem.Decode(data)
48+
if block != nil {
49+
input = block.Bytes
50+
}
51+
52+
// Try to load SubjectPublicKeyInfo
53+
pub, err0 := x509.ParsePKIXPublicKey(input)
54+
if err0 == nil {
55+
return pub, nil
56+
}
57+
58+
cert, err1 := x509.ParseCertificate(input)
59+
if err1 == nil {
60+
return cert.PublicKey, nil
61+
}
62+
63+
jwk, err2 := LoadJSONWebKey(data, true)
64+
if err2 == nil {
65+
return jwk, nil
66+
}
67+
68+
return nil, fmt.Errorf("square/go-jose: parse error, got '%s', '%s' and '%s'", err0, err1, err2)
69+
}
70+
71+
// LoadPrivateKey loads a private key from PEM/DER/JWK-encoded data.
72+
func LoadPrivateKey(data []byte) (interface{}, error) {
73+
input := data
74+
75+
block, _ := pem.Decode(data)
76+
if block != nil {
77+
input = block.Bytes
78+
}
79+
80+
var priv interface{}
81+
priv, err0 := x509.ParsePKCS1PrivateKey(input)
82+
if err0 == nil {
83+
return priv, nil
84+
}
85+
86+
priv, err1 := x509.ParsePKCS8PrivateKey(input)
87+
if err1 == nil {
88+
return priv, nil
89+
}
90+
91+
priv, err2 := x509.ParseECPrivateKey(input)
92+
if err2 == nil {
93+
return priv, nil
94+
}
95+
96+
jwk, err3 := LoadJSONWebKey(input, false)
97+
if err3 == nil {
98+
return jwk, nil
99+
}
100+
101+
return nil, fmt.Errorf("square/go-jose: parse error, got '%s', '%s', '%s' and '%s'", err0, err1, err2, err3)
102+
}

test/stub/ecdh.key

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-----BEGIN EC PRIVATE KEY-----
2+
MIGkAgEBBDDvoj/bM1HokUjYWO/IDFs26Jo0GIFtU3tMQQu7ZabKscDMK3dZA0mK
3+
v97ij7BBFbCgBwYFK4EEACKhZANiAAT3KhQQCDFN32y/B72g+qOFw/5/aNx1MvZa
4+
rwDDa/2G3V0HLTS0VE82sLEUKS8xwkWFI+gNRXk0vvN+Hf+myJI1jOIY+tYQlh+C
5+
ZiKGNJ6g5/Su7V6ukGtN+UiY+sx+0LI=
6+
-----END EC PRIVATE KEY-----

test/stub/ecdh.pub

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE9yoUEAgxTd9svwe9oPqjhcP+f2jcdTL2
3+
Wq8Aw2v9ht1dBy00tFRPNrCxFCkvMcJFhSPoDUV5NL7zfh3/psiSNYziGPrWEJYf
4+
gmYihjSeoOf0ru1erpBrTflImPrMftCy
5+
-----END PUBLIC KEY-----

0 commit comments

Comments
 (0)