diff --git a/handler/config/credentials_token.go b/handler/config/credentials_token.go new file mode 100644 index 00000000..368aa986 --- /dev/null +++ b/handler/config/credentials_token.go @@ -0,0 +1,43 @@ +/* +Copyright © 2020 Portworx + +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 configcli + +import ( + "github.com/portworx/pxc/pkg/commander" + "github.com/portworx/pxc/pkg/util" + "github.com/spf13/cobra" +) + +// tokenCmd represents the token command +var tokenCmd *cobra.Command + +var _ = commander.RegisterCommandVar(func() { + tokenCmd = &cobra.Command{ + Use: "token", + Short: "Portworx token management commands", + Run: func(cmd *cobra.Command, args []string) { + util.Printf("Please see pxc config credentials token --help for more commands\n") + }, + } +}) + +var _ = commander.RegisterCommandInit(func() { + CredentialsAddCommand(tokenCmd) +}) + +func CredentialsTokenAddCommand(cmd *cobra.Command) { + tokenCmd.AddCommand(cmd) +} diff --git a/handler/config/credentials_whoami.go b/handler/config/credentials_whoami.go new file mode 100644 index 00000000..e3661cbd --- /dev/null +++ b/handler/config/credentials_whoami.go @@ -0,0 +1,118 @@ +/* +Copyright © 2020 Portworx + +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 configcli + +import ( + "fmt" + "strings" + "time" + + "github.com/portworx/pxc/pkg/auth" + "github.com/portworx/pxc/pkg/commander" + "github.com/portworx/pxc/pkg/config" + "github.com/portworx/pxc/pkg/portworx" + "github.com/portworx/pxc/pkg/util" + "github.com/spf13/cobra" +) + +type whoamiOptions struct { + token string +} + +var ( + whoamiArgs *whoamiOptions + whoamiCmd *cobra.Command +) + +var _ = commander.RegisterCommandVar(func() { + whoamiArgs = &whoamiOptions{} + whoamiCmd = &cobra.Command{ + Use: "whoami", + Short: "Shows current authentication information", + RunE: whoAmIExec, + } +}) + +var _ = commander.RegisterCommandInit(func() { + CredentialsAddCommand(whoamiCmd) + whoamiCmd.Flags().StringVar(&whoamiArgs.token, + "auth-token", "", "Use this token instead of the token saved in the configuration. Useful for debugging tokens") +}) + +func WhoAmIAddCommand(cmd *cobra.Command) { + whoamiCmd.AddCommand(cmd) +} + +func whoAmIExec(cmd *cobra.Command, args []string) error { + token := whoamiArgs.token + if len(token) == 0 { + authInfo := config.CM().GetCurrentAuthInfo() + token = authInfo.Token + + if len(authInfo.KubernetesAuthInfo.SecretName) != 0 && + len(authInfo.KubernetesAuthInfo.SecretNamespace) != 0 { + var err error + token, err = portworx.PxGetTokenFromSecret(authInfo.KubernetesAuthInfo.SecretName, authInfo.KubernetesAuthInfo.SecretNamespace) + if err != nil { + return fmt.Errorf("Unable to retreive token from Kubernetes: %v", err) + } + } + if len(token) == 0 { + util.Printf("No authentication information provided") + return nil + } + } + + expTime, err := auth.GetExpiration(token) + if err != nil { + return fmt.Errorf("Unable to get expiration information from token: %v", err) + } + iatTime, err := auth.GetIssuedAtTime(token) + if err != nil { + return fmt.Errorf("Unable to get issued time information from token: %v", err) + } + claims, err := auth.TokenClaims(token) + if err != nil { + return err + } + + status := "Ok" + err = auth.ValidateToken(token) + if err != nil { + status = fmt.Sprintf("%v", err) + } + + util.Printf("Name: %s\n"+ + "Email: %s\n"+ + "Subject: %s\n"+ + "Groups: %s\n"+ + "Roles: %s\n"+ + "Issued At Time: %s\n"+ + "Expiration Time: %s\n"+ + "\n"+ + "Status: %s\n", + claims.Name, + claims.Email, + claims.Subject, + strings.Join(claims.Groups, ","), + strings.Join(claims.Roles, ","), + iatTime.Format(time.UnixDate), + expTime.Format(time.UnixDate), + status) + + return nil + +} diff --git a/handler/config/token_generate.go b/handler/config/token_generate.go new file mode 100644 index 00000000..1616fd39 --- /dev/null +++ b/handler/config/token_generate.go @@ -0,0 +1,160 @@ +/* +Copyright © 2020 Portworx + +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 configcli + +import ( + "fmt" + "strings" + "time" + + "github.com/portworx/pxc/pkg/auth" + "github.com/portworx/pxc/pkg/commander" + "github.com/portworx/pxc/pkg/util" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +type tokenInfo struct { + issuer string + subject string + name string + email string + roles string + groups string +} + +type tokenGenOptions struct { + sharedSecret string + rsaPem string + ecdsaPem string + duration string + output string + token tokenInfo +} + +// tokenGenCmd represents the tokenGen command +var ( + tokenGenArgs *tokenGenOptions + tokenGenCmd *cobra.Command +) + +var _ = commander.RegisterCommandVar(func() { + tokenGenArgs = &tokenGenOptions{} + tokenGenCmd = &cobra.Command{ + Use: "generate", + Aliases: []string{"gen"}, + Short: "Generate a Portworx token", + RunE: tokenGenExec, + } +}) + +var _ = commander.RegisterCommandInit(func() { + CredentialsTokenAddCommand(tokenGenCmd) + tokenGenCmd.Flags().StringVar(&tokenGenArgs.sharedSecret, + "shared-secret", "", "Shared secret to sign token") + tokenGenCmd.Flags().StringVar(&tokenGenArgs.rsaPem, + "rsa-private-keyfile", "", "RSA Private file to sign token") + tokenGenCmd.Flags().StringVar(&tokenGenArgs.ecdsaPem, + "ecdsa-private-keyfile", "", "ECDSA Private file to sign token") + tokenGenCmd.Flags().StringVar(&tokenGenArgs.duration, + "token-duration", "1d", "Duration of time where the token will be valid. "+ + "Postfix the duration by using "+ + auth.SecondDef+" for seconds, "+ + auth.MinuteDef+" for minutes, "+ + auth.HourDef+" for hours, "+ + auth.DayDef+" for days, and "+ + auth.YearDef+" for years.") + tokenGenCmd.Flags().StringVar(&tokenGenArgs.token.issuer, + "token-issuer", "portworx.com", + "Issuer name of token. Do not use https:// in the issuer since it could indicate "+ + "that this is an OpenID Connect issuer.") + tokenGenCmd.Flags().StringVar(&tokenGenArgs.token.name, + "token-name", "", "Account name") + tokenGenCmd.Flags().StringVar(&tokenGenArgs.token.subject, + "token-subject", "", "Unique ID of this account") + tokenGenCmd.Flags().StringVar(&tokenGenArgs.token.email, + "token-email", "", "Unique ID of this account") + tokenGenCmd.Flags().StringVar(&tokenGenArgs.token.roles, + "token-roles", "", "Comma separated list of roles applied to this token") + tokenGenCmd.Flags().StringVar(&tokenGenArgs.token.groups, + "token-groups", "", "Comma separated list of groups which the token will be part of") + +}) + +func TokenGenerateAddCommand(cmd *cobra.Command) { + tokenGenCmd.AddCommand(cmd) +} + +func tokenGenExec(cmd *cobra.Command, args []string) error { + + if len(tokenGenArgs.token.name) == 0 { + return fmt.Errorf("Must supply an account name") + } else if len(tokenGenArgs.token.email) == 0 { + return fmt.Errorf("Must supply an email address") + } else if len(tokenGenArgs.token.subject) == 0 { + return fmt.Errorf("Must supply a unique identifier as the subject") + } + if len(tokenGenArgs.token.roles) == 0 { + logrus.Warningf("Warning: No role provided") + } + if len(tokenGenArgs.token.groups) == 0 { + logrus.Warningf("Warning: No role provided") + } + + claims := &auth.Claims{ + Name: tokenGenArgs.token.name, + Email: tokenGenArgs.token.email, + Subject: tokenGenArgs.token.subject, + Roles: strings.Split(tokenGenArgs.token.roles, ","), + Groups: strings.Split(tokenGenArgs.token.groups, ","), + } + + // Get duration + options := &auth.Options{ + Issuer: tokenGenArgs.token.issuer, + } + expDuration, err := auth.ParseToDuration(tokenGenArgs.duration) + if err != nil { + return fmt.Errorf("Unable to parse duration") + } + options.Expiration = time.Now().Add(expDuration).Unix() + + // Get signature + var signature *auth.Signature + if len(tokenGenArgs.sharedSecret) != 0 { + signature, err = auth.NewSignatureSharedSecret(tokenGenArgs.sharedSecret) + } else if len(tokenGenArgs.rsaPem) != 0 { + signature, err = auth.NewSignatureRSAFromFile(tokenGenArgs.rsaPem) + } else if len(tokenGenArgs.ecdsaPem) != 0 { + signature, err = auth.NewSignatureECDSAFromFile(tokenGenArgs.ecdsaPem) + } else { + return fmt.Errorf("Must provide a secret key to sign token") + } + if err != nil { + return fmt.Errorf("Unable to generate signature: %v", err) + } + + // Generate token + token, err := auth.Token(claims, signature, options) + if err != nil { + return fmt.Errorf("Failed to create token: %v", err) + } + + // Print token + util.Printf("%s\n", token) + + return nil +} diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go new file mode 100644 index 00000000..9fec2d29 --- /dev/null +++ b/pkg/auth/auth.go @@ -0,0 +1,176 @@ +/* +Copyright 2018 Portworx + +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 auth + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + jwt "github.com/dgrijalva/jwt-go" +) + +// Claims provides information about the claims in the token +// See https://openid.net/specs/openid-connect-core-1_0.html#IDToken +// for more information. +type Claims struct { + // Issuer is the token issuer. For selfsigned token do not prefix + // with `https://`. + Issuer string `json:"iss"` + // Subject identifier. Unique ID of this account + Subject string `json:"sub" yaml:"sub"` + // Account name + Name string `json:"name" yaml:"name"` + // Account email + Email string `json:"email" yaml:"email"` + // Roles of this account + Roles []string `json:"roles,omitempty" yaml:"roles,omitempty"` + // (optional) Groups in which this account is part of + Groups []string `json:"groups,omitempty" yaml:"groups,omitempty"` +} + +// Options provide any options to apply to the token +type Options struct { + // Expiration time in Unix format as per JWT standard + Expiration int64 + // Issuer of the claims + Issuer string +} + +// Token returns a signed JWT containing the claims provided +func Token( + claims *Claims, + signature *Signature, + options *Options, +) (string, error) { + + mapclaims := jwt.MapClaims{ + "sub": claims.Subject, + "iss": options.Issuer, + "email": claims.Email, + "name": claims.Name, + "roles": claims.Roles, + "iat": time.Now().Unix(), + "exp": options.Expiration, + } + if claims.Groups != nil { + mapclaims["groups"] = claims.Groups + } + token := jwt.NewWithClaims(signature.Type, mapclaims) + signedtoken, err := token.SignedString(signature.Key) + if err != nil { + return "", err + } + + return signedtoken, nil +} + +// TokenClaims returns the claims for the raw JWT token. +func TokenClaims(rawtoken string) (*Claims, error) { + parts := strings.Split(rawtoken, ".") + + // There are supposed to be three parts for the token + if len(parts) < 3 { + return nil, fmt.Errorf("Token is invalid: %v", rawtoken) + } + + // Access claims in the token + claimBytes, err := jwt.DecodeSegment(parts[1]) + if err != nil { + return nil, fmt.Errorf("Failed to decode claims: %v", err) + } + var claims *Claims + + // Unmarshal claims + err = json.Unmarshal(claimBytes, &claims) + if err != nil { + return nil, fmt.Errorf("Unable to get information from the claims in the token: %v", err) + } + + return claims, nil +} + +// TokenIssuer returns the issuer for the raw JWT token. +func TokenIssuer(rawtoken string) (string, error) { + claims, err := TokenClaims(rawtoken) + if err != nil { + return "", err + } + + // Return issuer + if len(claims.Issuer) != 0 { + return claims.Issuer, nil + } else { + return "", fmt.Errorf("Issuer was not specified in the token") + } +} + +// IsJwtToken returns true if the provided string is a valid jwt token +func IsJwtToken(authstring string) bool { + _, _, err := new(jwt.Parser).ParseUnverified(authstring, jwt.MapClaims{}) + return err == nil +} + +func ValidateToken(rawtoken string) error { + var mapClaims jwt.MapClaims + _, _, err := new(jwt.Parser). + ParseUnverified(rawtoken, &mapClaims) + if err != nil { + return err + } + return mapClaims.Valid() +} + +// This function is similar to jwt-go's (m MapClaims) VerifyExpiresAt +// Copyright (c) 2012 Dave Grijalva +func GetExpiration(rawtoken string) (time.Time, error) { + var mapClaims jwt.MapClaims + _, _, err := new(jwt.Parser). + ParseUnverified(rawtoken, &mapClaims) + if err != nil { + return time.Time{}, err + } + + switch exp := mapClaims["exp"].(type) { + case float64: + return time.Unix(int64(exp), 0), nil + case json.Number: + v, _ := exp.Int64() + return time.Unix(v, 0), nil + } + return time.Time{}, fmt.Errorf("Unable to get expiration time from token") +} + +// This function is similar to jwt-go's (m MapClaims) VerifyIssedAt +// Copyright (c) 2012 Dave Grijalva +func GetIssuedAtTime(rawtoken string) (time.Time, error) { + var mapClaims jwt.MapClaims + _, _, err := new(jwt.Parser). + ParseUnverified(rawtoken, &mapClaims) + if err != nil { + return time.Time{}, err + } + + switch iat := mapClaims["iat"].(type) { + case float64: + return time.Unix(int64(iat), 0), nil + case json.Number: + v, _ := iat.Int64() + return time.Unix(v, 0), nil + } + return time.Time{}, fmt.Errorf("Unable to get expiration time from token") +} diff --git a/pkg/auth/auth_test.go b/pkg/auth/auth_test.go new file mode 100644 index 00000000..8d25c596 --- /dev/null +++ b/pkg/auth/auth_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2018 Portworx + +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 auth + +import ( + "testing" + "time" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/stretchr/testify/assert" +) + +func TestTokenSharedSecretSimple(t *testing.T) { + + key := []byte("mysecret") + claims := Claims{ + Email: "my@email.com", + Name: "myname", + Roles: []string{"hello"}, + } + sig := Signature{ + Type: jwt.SigningMethodHS256, + Key: key, + } + opts := Options{ + Expiration: time.Now().Add(time.Minute * 10).Unix(), + } + + // Create + rawtoken, err := Token(&claims, &sig, &opts) + assert.NoError(t, err) + assert.NotEmpty(t, rawtoken) + + // Verify + token, err := jwt.Parse(rawtoken, func(token *jwt.Token) (interface{}, error) { + return key, nil + }) + assert.True(t, token.Valid) + tokenClaims, ok := token.Claims.(jwt.MapClaims) + assert.True(t, ok) + assert.Contains(t, tokenClaims, "email") + assert.Equal(t, claims.Email, tokenClaims["email"]) + assert.Contains(t, tokenClaims, "name") + assert.Equal(t, claims.Name, tokenClaims["name"]) + assert.Contains(t, tokenClaims, "roles") + assert.Equal(t, claims.Roles[0], tokenClaims["roles"].([]interface{})[0].(string)) +} + +func TestTokenExpired(t *testing.T) { + + key := []byte("mysecret") + claims := Claims{} + sig := Signature{ + Type: jwt.SigningMethodHS256, + Key: key, + } + opts := Options{ + Expiration: time.Now().Add(-(time.Minute * 10)).Unix(), + } + + // Create + rawtoken, err := Token(&claims, &sig, &opts) + assert.NoError(t, err) + assert.NotEmpty(t, rawtoken) + + // Verify + token, err := jwt.Parse(rawtoken, func(token *jwt.Token) (interface{}, error) { + return key, nil + }) + assert.False(t, token.Valid) +} diff --git a/pkg/auth/duration.go b/pkg/auth/duration.go new file mode 100644 index 00000000..787e3c90 --- /dev/null +++ b/pkg/auth/duration.go @@ -0,0 +1,85 @@ +/* +Copyright 2018 Portworx + +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 auth + +import ( + "fmt" + "regexp" + "strconv" + "time" +) + +const ( + SecondDef = "s" + MinuteDef = "m" + HourDef = "h" + DayDef = "d" + YearDef = "y" + + Day = time.Hour * 24 + Year = Day * 365 +) + +var ( + SecondRegex = regexp.MustCompile("([0-9]+)" + SecondDef) + MinuteRegex = regexp.MustCompile("([0-9]+)" + MinuteDef) + HourRegex = regexp.MustCompile("([0-9]+)" + HourDef) + DayRegex = regexp.MustCompile("([0-9]+)" + DayDef) + YearRegex = regexp.MustCompile("([0-9]+)" + YearDef) +) + +// ParseToDuration takes in a "human" type duration and changes it to +// time.Duration. The format for a human type is . For +// example: Five days: 5d; one year: 1y. +func ParseToDuration(s string) (time.Duration, error) { + + regexs := []struct { + regex *regexp.Regexp + duration time.Duration + }{ + { + regex: SecondRegex, + duration: time.Second, + }, + { + regex: MinuteRegex, + duration: time.Minute, + }, + { + regex: HourRegex, + duration: time.Hour, + }, + { + regex: DayRegex, + duration: Day, + }, + { + regex: YearRegex, + duration: Year, + }, + } + for _, r := range regexs { + if val := r.regex.FindString(s); len(val) != 0 { + parsed, err := strconv.Atoi(val[:len(val)-1]) + if err != nil { + return 0, err + } + return time.Duration(parsed) * r.duration, nil + } + } + + return 0, fmt.Errorf("Unable to parse") +} diff --git a/pkg/auth/duration_test.go b/pkg/auth/duration_test.go new file mode 100644 index 00000000..4a9f1b75 --- /dev/null +++ b/pkg/auth/duration_test.go @@ -0,0 +1,77 @@ +/* +Copyright 2018 Portworx + +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 auth + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestParseDuration(t *testing.T) { + tests := []struct { + s string + expectedDuration time.Duration + expectFail bool + }{ + { + s: "123y", + expectedDuration: 123 * time.Hour * 24 * 365, + }, + { + s: "123d", + expectedDuration: 123 * time.Hour * 24, + }, + { + s: "123h", + expectedDuration: 123 * time.Hour, + }, + { + s: "123m", + expectedDuration: 123 * time.Minute, + }, + { + s: "123s", + expectedDuration: 123 * time.Second, + }, + { + s: "123", + expectedDuration: 0, + expectFail: true, + }, + { + s: "", + expectedDuration: 0, + expectFail: true, + }, + { + s: "12134212342342347239847asdasdf", + expectedDuration: 0, + expectFail: true, + }, + } + + for _, test := range tests { + duration, err := ParseToDuration(test.s) + if test.expectFail { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, test.expectedDuration, duration, test.s) + } + } +} diff --git a/pkg/auth/signature.go b/pkg/auth/signature.go new file mode 100644 index 00000000..d57dff43 --- /dev/null +++ b/pkg/auth/signature.go @@ -0,0 +1,75 @@ +/* +Copyright 2018 Portworx + +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 auth + +import ( + "fmt" + "io/ioutil" + + jwt "github.com/dgrijalva/jwt-go" +) + +// Signature describes the signature type using definitions from +// the jwt package +type Signature struct { + Type jwt.SigningMethod + Key interface{} +} + +func NewSignatureSharedSecret(secret string) (*Signature, error) { + return &Signature{ + Key: []byte(secret), + Type: jwt.SigningMethodHS256, + }, nil +} + +func NewSignatureRSAFromFile(filename string) (*Signature, error) { + pem, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("Failed to read RSA file: %v", err) + } + return NewSignatureRSA(pem) +} + +func NewSignatureRSA(pem []byte) (*Signature, error) { + var err error + signature := &Signature{} + signature.Key, err = jwt.ParseRSAPrivateKeyFromPEM(pem) + if err != nil { + return nil, fmt.Errorf("Failed to parse RSA file: %v", err) + } + signature.Type = jwt.SigningMethodRS256 + return signature, nil +} + +func NewSignatureECDSAFromFile(filename string) (*Signature, error) { + pem, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("Failed to read ECDSA file: %v", err) + } + return NewSignatureECDSA(pem) +} + +func NewSignatureECDSA(pem []byte) (*Signature, error) { + var err error + signature := &Signature{} + signature.Key, err = jwt.ParseECPrivateKeyFromPEM(pem) + if err != nil { + return nil, fmt.Errorf("Failed to parse ECDSA file: %v", err) + } + signature.Type = jwt.SigningMethodES256 + return signature, nil +}