pkg/cmd/jwks/root.go (152 lines of code) (raw):
package jwks
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"os"
"github.com/pkg/errors"
"github.com/spf13/cobra"
jose "gopkg.in/square/go-jose.v2"
"k8s.io/client-go/util/keyutil"
"monis.app/mlog"
)
type jwksCmd struct {
publicKeys []string
outputFile string
}
// NewJWKSCmd returns a new serviceaccount command
func NewJWKSCmd() *cobra.Command {
jwksCmd := &jwksCmd{}
cmd := &cobra.Command{
Use: "jwks",
Short: "JSON Web Key Sets for the service account issuer keys",
Long: "This command provides the ability to generate a JSON Web Key Sets (JWKS) for the service account issuer keys",
PreRunE: func(cmd *cobra.Command, args []string) error {
return jwksCmd.validate()
},
RunE: func(cmd *cobra.Command, args []string) error {
return jwksCmd.run()
},
}
f := cmd.Flags()
f.StringSliceVar(&jwksCmd.publicKeys, "public-keys", nil, "List of public keys to include in the JWKS")
f.StringVar(&jwksCmd.outputFile, "output-file", "", "The name of the file to write the JWKS to. If not provided, the default output is stdout")
_ = cmd.MarkFlagRequired("public-keys")
return cmd
}
func (jc *jwksCmd) validate() error {
if len(jc.publicKeys) == 0 {
return errors.New("no public keys provided")
}
return nil
}
func (jc *jwksCmd) run() error {
mlog.Debug("generating JSON Web Key Set", "publicKeys", jc.publicKeys)
var pubKeys []interface{}
for _, file := range jc.publicKeys {
pubKey, err := keyutil.PublicKeysFromFile(file)
if err != nil {
return errors.Wrap(err, "failed to read public key file")
}
pubKeys = append(pubKeys, pubKey...)
}
keySet, err := publicJWKSFromKeys(pubKeys)
if err != nil {
return errors.Wrap(err, "failed to construct JSONWebKeySet from a list of keys")
}
keysetJSON, err := json.MarshalIndent(keySet, "", " ")
if err != nil {
return errors.Wrap(err, "failed to marshal JSONWebKeySet")
}
if jc.outputFile != "" {
// write the keyset to the file
if err = os.WriteFile(jc.outputFile, keysetJSON, 0600); err != nil {
return errors.Wrap(err, "failed to write JWKS to file")
}
mlog.Debug("wrote JWKS", "file", jc.outputFile)
return nil
}
mlog.Debug("writing JWKS to stdout")
// write the keyset to stdout
if _, err = os.Stdout.Write(keysetJSON); err != nil {
return errors.Wrap(err, "failed to write JWKS to stdout")
}
return nil
}
// Most of the changes here have been vendored from pkg/serviceaccount/openidmetadata.go
// * link: https://github.com/kubernetes/kubernetes/blob/ea0764452222146c47ec826977f49d7001b0ea8c/pkg/serviceaccount/openidmetadata.go
type publicKeyGetter interface {
Public() crypto.PublicKey
}
// publicJWKSFromKeys constructs a JSONWebKeySet from a list of keys. The key
// set will only contain the public keys associated with the input keys.
func publicJWKSFromKeys(in []interface{}) (*jose.JSONWebKeySet, error) {
// Decode keys into a JWKS.
var keys jose.JSONWebKeySet
for _, key := range in {
var pubkey *jose.JSONWebKey
var err error
switch k := key.(type) {
case publicKeyGetter:
// This is a private key. Get its public key
pubkey, err = jwkFromPublicKey(k.Public())
default:
pubkey, err = jwkFromPublicKey(k)
}
if err != nil {
return nil, err
}
if !pubkey.Valid() {
return nil, errors.New("the public key is not valid")
}
keys.Keys = append(keys.Keys, *pubkey)
}
return &keys, nil
}
func jwkFromPublicKey(publicKey crypto.PublicKey) (*jose.JSONWebKey, error) {
alg, err := algorithmFromPublicKey(publicKey)
if err != nil {
return nil, err
}
keyID, err := keyIDFromPublicKey(publicKey)
if err != nil {
return nil, err
}
jwk := &jose.JSONWebKey{
Algorithm: string(alg),
Key: publicKey,
KeyID: keyID,
Use: "sig",
}
if !jwk.IsPublic() {
return nil, errors.Errorf("JWK was not a public key! JWK: %v", jwk)
}
return jwk, nil
}
func algorithmFromPublicKey(publicKey crypto.PublicKey) (jose.SignatureAlgorithm, error) {
switch pk := publicKey.(type) {
case *rsa.PublicKey:
// IMPORTANT: If this function is updated to support additional key sizes,
// signerFromRSAPrivateKey in serviceaccount/jwt.go must also be
// updated to support the same key sizes. Today we only support RS256.
return jose.RS256, nil
case *ecdsa.PublicKey:
switch pk.Curve {
case elliptic.P256():
return jose.ES256, nil
case elliptic.P384():
return jose.ES384, nil
case elliptic.P521():
return jose.ES512, nil
default:
return "", errors.New("unknown private key curve, must be 256, 384, or 521")
}
case jose.OpaqueSigner:
return jose.SignatureAlgorithm(pk.Public().Algorithm), nil
default:
return "", errors.New("unknown public key type, must be *rsa.PublicKey, *ecdsa.PublicKey, or jose.OpaqueSigner")
}
}
// keyIDFromPublicKey derives a key ID non-reversibly from a public key.
//
// The Key ID is field on a given on JWTs and JWKs that help relying parties
// pick the correct key for verification when the identity party advertises
// multiple keys.
//
// Making the derivation non-reversible makes it impossible for someone to
// accidentally obtain the real key from the key ID and use it for token
// validation.
func keyIDFromPublicKey(publicKey interface{}) (string, error) {
publicKeyDERBytes, err := x509.MarshalPKIXPublicKey(publicKey)
if err != nil {
return "", errors.Wrap(err, "failed to serialize public key to DER format")
}
hasher := crypto.SHA256.New()
hasher.Write(publicKeyDERBytes)
publicKeyDERHash := hasher.Sum(nil)
keyID := base64.RawURLEncoding.EncodeToString(publicKeyDERHash)
return keyID, nil
}