Skip to content

Commit

Permalink
cmd: Allows import of PEM/DER/JSON encoded keys
Browse files Browse the repository at this point in the history
Closes #98

Signed-off-by: arekkas <aeneas@ory.am>
  • Loading branch information
arekkas authored and arekkas committed Jul 11, 2018
1 parent 3bbd5e8 commit 312f8d1
Show file tree
Hide file tree
Showing 14 changed files with 326 additions and 14 deletions.
2 changes: 1 addition & 1 deletion cmd/cli/handler_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func newClientHandler(c *config.Config) *ClientHandler {
}

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

fakeTlsTermination, _ := cmd.Flags().GetBool("skip-tls-verify")
c.Configuration.Transport = &http.Transport{
Expand Down
9 changes: 6 additions & 3 deletions cmd/cli/handler_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ import (
)

func checkResponse(response *hydra.APIResponse, err error, expectedStatusCode int) {
pkg.Must(err, "Command failed because error \"%s\" occurred.\n", err)
if response != nil {
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)
} else {
pkg.Must(err, "Command failed because error \"%s\" occurred and no response is available.\n", err)
}

if response.StatusCode != expectedStatusCode {
fmt.Fprintf(os.Stderr, "Command failed because status code %d was expeceted but code %d was received.\n", expectedStatusCode, response.StatusCode)
fmt.Fprintf(os.Stderr, "The server responded with:\n%s\n", response.Payload)
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)
os.Exit(1)
return
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/cli/handler_introspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (h *IntrospectionHandler) Introspect(cmd *cobra.Command, args []string) {
return
}

c := hydra.NewOAuth2ApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlash(cmd))
c := hydra.NewOAuth2ApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlashOrFail(cmd))

clientID, _ := cmd.Flags().GetString("client-id")
clientSecret, _ := cmd.Flags().GetString("client-secret")
Expand Down
118 changes: 117 additions & 1 deletion cmd/cli/handler_jwk.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,29 @@
package cli

import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"

"github.com/mendsley/gojwk"
"github.com/ory/hydra/config"
"github.com/ory/hydra/pkg"
hydra "github.com/ory/hydra/sdk/go/hydra/swagger"
"github.com/pborman/uuid"
"github.com/spf13/cobra"
"gopkg.in/square/go-jose.v2"
)

type JWKHandler struct {
Config *config.Config
}

func (h *JWKHandler) newJwkManager(cmd *cobra.Command) *hydra.JsonWebKeyApi {
c := hydra.NewJsonWebKeyApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlash(cmd))
c := hydra.NewJsonWebKeyApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlashOrFail(cmd))

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

func toSDKFriendlyJSONWebKey(key interface{}, kid string, use string, public bool) jose.JSONWebKey {
if jwk, ok := key.(*jose.JSONWebKey); ok {
key = jwk.Key
if jwk.KeyID != "" {
kid = jwk.KeyID
}
if jwk.Use != "" {
use = jwk.Use
}
}

var err error
var jwk *gojwk.Key
if public {
jwk, err = gojwk.PublicKey(key)
pkg.Must(err, "Unable to convert public key to JSON Web Key because %s", err)
} else {
jwk, err = gojwk.PrivateKey(key)
pkg.Must(err, "Unable to convert private key to JSON Web Key because %s", err)
}

return jose.JSONWebKey{
KeyID: kid,
Use: use,
Algorithm: jwk.Alg,
Key: key,
}
}

func (h *JWKHandler) ImportKeys(cmd *cobra.Command, args []string) {
if len(args) < 2 {
fmt.Println(cmd.UsageString())
return
}

id := args[0]
use, _ := cmd.Flags().GetString("use")
client := &http.Client{}

if skipTLSTermination, _ := cmd.Flags().GetBool("skip-tls-verify"); skipTLSTermination {
client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTLSTermination}}
}

u := h.Config.GetClusterURLWithoutTailingSlashOrFail(cmd) + "/keys/" + id
request, err := http.NewRequest("GET", u, nil)
pkg.Must(err, "Unable to initialize HTTP request")

if term, _ := cmd.Flags().GetBool("fake-tls-termination"); term {
request.Header.Set("X-Forwarded-Proto", "https")
}

if token, _ := cmd.Flags().GetString("access-token"); token != "" {
request.Header.Set("Authorization", "Bearer "+token)
}

response, err := client.Do(request)
pkg.Must(err, "Unable to fetch data from %s because %s", u, err)
defer response.Body.Close()

if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNotFound {
fmt.Printf("Expected status code 200 or 404 but got %d while fetching data from %s.\n", response.StatusCode, u)
os.Exit(1)
}

var set jose.JSONWebKeySet
pkg.Must(json.NewDecoder(response.Body).Decode(&set), "Unable to decode payload to JSON")

for _, path := range args[1:] {
file, err := ioutil.ReadFile(path)
pkg.Must(err, "Unable to read file %s", path)

if key, privateErr := pkg.LoadPrivateKey(file); privateErr != nil {
key, publicErr := pkg.LoadPublicKey(file)
if publicErr != nil {
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)
os.Exit(1)
}

set.Keys = append(set.Keys, toSDKFriendlyJSONWebKey(key, "public:"+uuid.New(), use, true))
} else {
set.Keys = append(set.Keys, toSDKFriendlyJSONWebKey(key, "private:"+uuid.New(), use, false))
}

fmt.Printf("Successfully loaded key from file %s\n", path)
}

body, err := json.Marshal(&set)
pkg.Must(err, "Unable to encode JSON Web Keys to JSON")

request, err = http.NewRequest("PUT", u, bytes.NewReader(body))
pkg.Must(err, "Unable to initialize HTTP request")

if term, _ := cmd.Flags().GetBool("fake-tls-termination"); term {
request.Header.Set("X-Forwarded-Proto", "https")
}

if token, _ := cmd.Flags().GetString("access-token"); token != "" {
request.Header.Set("Authorization", "Bearer "+token)
}
request.Header.Set("Content-Type", "application/json")

response, err = client.Do(request)
pkg.Must(err, "Unable to post data to %s because %s", u, err)
defer response.Body.Close()

fmt.Println("Keys successfully imported!")
}

func (h *JWKHandler) GetKeys(cmd *cobra.Command, args []string) {
m := h.newJwkManager(cmd)
if len(args) != 1 {
Expand Down
4 changes: 2 additions & 2 deletions cmd/cli/handler_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type TokenHandler struct {
}

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

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

handler := hydra.NewOAuth2ApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlash(cmd))
handler := hydra.NewOAuth2ApiWithBasePath(h.Config.GetClusterURLWithoutTailingSlashOrFail(cmd))

skipTLSTermination, _ := cmd.Flags().GetBool("skip-tls-verify")
handler.Configuration.Transport = &http.Transport{
Expand Down
42 changes: 42 additions & 0 deletions cmd/keys_import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright © 2018 NAME HERE <EMAIL ADDRESS>
//
// 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// 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.

package cmd

import (
"github.com/spf13/cobra"
)

// keysImportCmd represents the import command
var keysImportCmd = &cobra.Command{
Use: "import <set> <file-1> [<file-2> [<file-3 [<...>]]]",
Short: "Imports cryptographic keys of any format to the JSON Web Key Store",
Long: `This command allows you to import cryptographic keys to the JSON Web Key Store.
Currently supported formats are raw JSON Web Keys or PEM/DER encoded data. If the JSON Web Key Set exists already,
the imported keys will be added to that set. Otherwise, a new set will be created.
Please be aware that importing a private key does not automatically import its public key as well.
Examples:
hydra keys import my-set ./path/to/jwk.json ./path/to/jwk-2.json
hydra keys import my-set ./path/to/rsa.key ./path/to/rsa.pub
`,
Run: cmdHandler.Keys.ImportKeys,
}

func init() {
keysCmd.AddCommand(keysImportCmd)
keysImportCmd.Flags().String("use", "sig", "Sets the \"use\" value of the JSON Web Key if not \"use\" value was defined by the key itself")
}
2 changes: 2 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ func TestExecute(t *testing.T) {
{args: []string{"keys", "rotate", "--endpoint", endpoint, "foo"}},
{args: []string{"keys", "get", "--endpoint", endpoint, "foo"}},
{args: []string{"keys", "delete", "--endpoint", endpoint, "foo"}},
{args: []string{"keys", "import", "--endpoint", endpoint, "import-1", "../test/stub/ecdh.key", "../test/stub/ecdh.pub"}},
{args: []string{"keys", "import", "--endpoint", endpoint, "import-2", "../test/stub/rsa.key", "../test/stub/rsa.pub"}},
{args: []string{"token", "revoke", "--endpoint", endpoint, "--client-secret", "foobar", "--client-id", "foobarbaz", "foo"}},
{args: []string{"token", "client", "--endpoint", endpoint, "--client-secret", "foobar", "--client-id", "foobarbaz"}},
{args: []string{"help", "migrate", "sql"}},
Expand Down
4 changes: 2 additions & 2 deletions cmd/token_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ var tokenClientCmd = &cobra.Command{

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

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

clientID, _ := cmd.Flags().GetString("client-id")
clientSecret, _ := cmd.Flags().GetString("client-secret")
Expand Down
8 changes: 4 additions & 4 deletions cmd/token_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@ var tokenUserCmd = &cobra.Command{
}

if backend == "" {
bu, err := url.Parse(c.GetClusterURLWithoutTailingSlash(cmd))
pkg.Must(err, `Unable to parse cluster url ("%s"): %s`, c.GetClusterURLWithoutTailingSlash(cmd), err)
bu, err := url.Parse(c.GetClusterURLWithoutTailingSlashOrFail(cmd))
pkg.Must(err, `Unable to parse cluster url ("%s"): %s`, c.GetClusterURLWithoutTailingSlashOrFail(cmd), err)
backend = urlx.AppendPaths(bu, "/oauth2/token").String()
}
if frontend == "" {
fu, err := url.Parse(c.GetClusterURLWithoutTailingSlash(cmd))
pkg.Must(err, `Unable to parse cluster url ("%s"): %s`, c.GetClusterURLWithoutTailingSlash(cmd), err)
fu, err := url.Parse(c.GetClusterURLWithoutTailingSlashOrFail(cmd))
pkg.Must(err, `Unable to parse cluster url ("%s"): %s`, c.GetClusterURLWithoutTailingSlashOrFail(cmd), err)
frontend = urlx.AppendPaths(fu, "/oauth2/auth").String()
}

Expand Down
102 changes: 102 additions & 0 deletions pkg/jose-utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*-
* Copyright 2014 Square Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/

package pkg

import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"

"gopkg.in/square/go-jose.v2"
)

func LoadJSONWebKey(json []byte, pub bool) (*jose.JSONWebKey, error) {
var jwk jose.JSONWebKey
err := jwk.UnmarshalJSON(json)
if err != nil {
return nil, err
}
if !jwk.Valid() {
return nil, errors.New("invalid JWK key")
}
if jwk.IsPublic() != pub {
return nil, errors.New("priv/pub JWK key mismatch")
}
return &jwk, nil
}

// LoadPublicKey loads a public key from PEM/DER/JWK-encoded data.
func LoadPublicKey(data []byte) (interface{}, error) {
input := data

block, _ := pem.Decode(data)
if block != nil {
input = block.Bytes
}

// Try to load SubjectPublicKeyInfo
pub, err0 := x509.ParsePKIXPublicKey(input)
if err0 == nil {
return pub, nil
}

cert, err1 := x509.ParseCertificate(input)
if err1 == nil {
return cert.PublicKey, nil
}

jwk, err2 := LoadJSONWebKey(data, true)
if err2 == nil {
return jwk, nil
}

return nil, fmt.Errorf("square/go-jose: parse error, got '%s', '%s' and '%s'", err0, err1, err2)
}

// LoadPrivateKey loads a private key from PEM/DER/JWK-encoded data.
func LoadPrivateKey(data []byte) (interface{}, error) {
input := data

block, _ := pem.Decode(data)
if block != nil {
input = block.Bytes
}

var priv interface{}
priv, err0 := x509.ParsePKCS1PrivateKey(input)
if err0 == nil {
return priv, nil
}

priv, err1 := x509.ParsePKCS8PrivateKey(input)
if err1 == nil {
return priv, nil
}

priv, err2 := x509.ParseECPrivateKey(input)
if err2 == nil {
return priv, nil
}

jwk, err3 := LoadJSONWebKey(input, false)
if err3 == nil {
return jwk, nil
}

return nil, fmt.Errorf("square/go-jose: parse error, got '%s', '%s', '%s' and '%s'", err0, err1, err2, err3)
}
6 changes: 6 additions & 0 deletions test/stub/ecdh.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDDvoj/bM1HokUjYWO/IDFs26Jo0GIFtU3tMQQu7ZabKscDMK3dZA0mK
v97ij7BBFbCgBwYFK4EEACKhZANiAAT3KhQQCDFN32y/B72g+qOFw/5/aNx1MvZa
rwDDa/2G3V0HLTS0VE82sLEUKS8xwkWFI+gNRXk0vvN+Hf+myJI1jOIY+tYQlh+C
ZiKGNJ6g5/Su7V6ukGtN+UiY+sx+0LI=
-----END EC PRIVATE KEY-----
5 changes: 5 additions & 0 deletions test/stub/ecdh.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE9yoUEAgxTd9svwe9oPqjhcP+f2jcdTL2
Wq8Aw2v9ht1dBy00tFRPNrCxFCkvMcJFhSPoDUV5NL7zfh3/psiSNYziGPrWEJYf
gmYihjSeoOf0ru1erpBrTflImPrMftCy
-----END PUBLIC KEY-----
Loading

0 comments on commit 312f8d1

Please sign in to comment.