mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-19 06:31:47 +00:00
Merge branch 'main' into label_every_resource
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package supervisorconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
configv1alpha1 "go.pinniped.dev/generated/1.19/apis/config/v1alpha1"
|
||||
pinnipedclientset "go.pinniped.dev/generated/1.19/client/clientset/versioned"
|
||||
configinformers "go.pinniped.dev/generated/1.19/client/informers/externalversions/config/v1alpha1"
|
||||
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
)
|
||||
|
||||
// These constants are the keys in an OPC's Secret's Data map.
|
||||
const (
|
||||
// activeJWKKey points to the current private key used for signing tokens.
|
||||
//
|
||||
// Note! The value for this key will contain private key material!
|
||||
activeJWKKey = "activeJWK"
|
||||
// jwksKey points to the current JWKS used to verify tokens.
|
||||
//
|
||||
// Note! The value for this key will contain only public key material!
|
||||
jwksKey = "jwks"
|
||||
)
|
||||
|
||||
const (
|
||||
opcKind = "OIDCProviderConfig"
|
||||
)
|
||||
|
||||
// generateKey is stubbed out for the purpose of testing. The default behavior is to generate an EC key.
|
||||
//nolint:gochecknoglobals
|
||||
var generateKey func(r io.Reader) (interface{}, error) = generateECKey
|
||||
|
||||
func generateECKey(r io.Reader) (interface{}, error) {
|
||||
return ecdsa.GenerateKey(elliptic.P256(), r)
|
||||
}
|
||||
|
||||
// jwkController holds the fields necessary for the JWKS controller to communicate with OPC's and
|
||||
// secrets, both via a cache and via the API.
|
||||
type jwksController struct {
|
||||
pinnipedClient pinnipedclientset.Interface
|
||||
kubeClient kubernetes.Interface
|
||||
opcInformer configinformers.OIDCProviderConfigInformer
|
||||
secretInformer corev1informers.SecretInformer
|
||||
}
|
||||
|
||||
// NewJWKSController returns a controllerlib.Controller that ensures an OPC has a corresponding
|
||||
// Secret that contains a valid active JWK and JWKS.
|
||||
func NewJWKSController(
|
||||
kubeClient kubernetes.Interface,
|
||||
pinnipedClient pinnipedclientset.Interface,
|
||||
secretInformer corev1informers.SecretInformer,
|
||||
opcInformer configinformers.OIDCProviderConfigInformer,
|
||||
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
||||
) controllerlib.Controller {
|
||||
return controllerlib.New(
|
||||
controllerlib.Config{
|
||||
Name: "JWKSController",
|
||||
Syncer: &jwksController{
|
||||
kubeClient: kubeClient,
|
||||
pinnipedClient: pinnipedClient,
|
||||
secretInformer: secretInformer,
|
||||
opcInformer: opcInformer,
|
||||
},
|
||||
},
|
||||
// We want to be notified when a OPC's secret gets updated or deleted. When this happens, we
|
||||
// should get notified via the corresponding OPC key.
|
||||
withInformer(
|
||||
secretInformer,
|
||||
controllerlib.FilterFuncs{
|
||||
ParentFunc: func(obj metav1.Object) controllerlib.Key {
|
||||
if isOPCControllee(obj) {
|
||||
controller := metav1.GetControllerOf(obj)
|
||||
return controllerlib.Key{
|
||||
Name: controller.Name,
|
||||
Namespace: obj.GetNamespace(),
|
||||
}
|
||||
}
|
||||
return controllerlib.Key{}
|
||||
},
|
||||
AddFunc: isOPCControllee,
|
||||
UpdateFunc: func(oldObj, newObj metav1.Object) bool {
|
||||
return isOPCControllee(oldObj) || isOPCControllee(newObj)
|
||||
},
|
||||
DeleteFunc: isOPCControllee,
|
||||
},
|
||||
controllerlib.InformerOption{},
|
||||
),
|
||||
// We want to be notified when anything happens to an OPC.
|
||||
withInformer(
|
||||
opcInformer,
|
||||
pinnipedcontroller.NoOpFilter(),
|
||||
controllerlib.InformerOption{},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Sync implements controllerlib.Syncer.
|
||||
func (c *jwksController) Sync(ctx controllerlib.Context) error {
|
||||
opc, err := c.opcInformer.Lister().OIDCProviderConfigs(ctx.Key.Namespace).Get(ctx.Key.Name)
|
||||
notFound := k8serrors.IsNotFound(err)
|
||||
if err != nil && !notFound {
|
||||
return fmt.Errorf(
|
||||
"failed to get %s/%s OIDCProviderConfig: %w",
|
||||
ctx.Key.Namespace,
|
||||
ctx.Key.Name,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if notFound {
|
||||
// The corresponding secret to this OPC should have been garbage collected since it should have
|
||||
// had this OPC as its owner.
|
||||
klog.InfoS(
|
||||
"oidcproviderconfig deleted",
|
||||
"oidcproviderconfig",
|
||||
klog.KRef(ctx.Key.Namespace, ctx.Key.Name),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
secretNeedsUpdate, err := c.secretNeedsUpdate(opc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine secret status: %w", err)
|
||||
}
|
||||
if !secretNeedsUpdate {
|
||||
// Secret is up to date - we are good to go.
|
||||
klog.InfoS(
|
||||
"secret is up to date",
|
||||
"oidcproviderconfig",
|
||||
klog.KRef(ctx.Key.Namespace, ctx.Key.Name),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the OPC does not have a secret associated with it, that secret does not exist, or the secret
|
||||
// is invalid, we will generate a new secret (i.e., a JWKS).
|
||||
secret, err := c.generateSecret(opc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot generate secret: %w", err)
|
||||
}
|
||||
|
||||
if err := c.createOrUpdateSecret(ctx.Context, secret); err != nil {
|
||||
return fmt.Errorf("cannot create or update secret: %w", err)
|
||||
}
|
||||
klog.InfoS("created/updated secret", "secret", klog.KObj(secret))
|
||||
|
||||
// Ensure that the OPC points to the secret.
|
||||
newOPC := opc.DeepCopy()
|
||||
newOPC.Status.JWKSSecret.Name = secret.Name
|
||||
if err := c.updateOPC(ctx.Context, newOPC); err != nil {
|
||||
return fmt.Errorf("cannot update opc: %w", err)
|
||||
}
|
||||
klog.InfoS("updated oidcproviderconfig", "oidcproviderconfig", klog.KObj(newOPC))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *jwksController) secretNeedsUpdate(opc *configv1alpha1.OIDCProviderConfig) (bool, error) {
|
||||
if opc.Status.JWKSSecret.Name == "" {
|
||||
// If the OPC says it doesn't have a secret associated with it, then let's create one.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// This OPC says it has a secret associated with it. Let's try to get it from the cache.
|
||||
secret, err := c.secretInformer.Lister().Secrets(opc.Namespace).Get(opc.Status.JWKSSecret.Name)
|
||||
notFound := k8serrors.IsNotFound(err)
|
||||
if err != nil && !notFound {
|
||||
return false, fmt.Errorf("cannot get secret: %w", err)
|
||||
}
|
||||
if notFound {
|
||||
// If we can't find the secret, let's assume we need to create it.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !isValid(secret) {
|
||||
// If this secret is invalid, we need to generate a new one.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (c *jwksController) generateSecret(opc *configv1alpha1.OIDCProviderConfig) (*corev1.Secret, error) {
|
||||
// Note! This is where we could potentially add more handling of OPC spec fields which tell us how
|
||||
// this OIDC provider should sign and verify ID tokens (e.g., hardcoded token secret, gRPC
|
||||
// connection to KMS, etc).
|
||||
//
|
||||
// For now, we just generate an new RSA keypair and put that in the secret.
|
||||
|
||||
key, err := generateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot generate key: %w", err)
|
||||
}
|
||||
|
||||
jwk := jose.JSONWebKey{
|
||||
Key: key,
|
||||
KeyID: "pinniped-supervisor-key",
|
||||
Algorithm: "ES256",
|
||||
Use: "sig",
|
||||
}
|
||||
jwkData, err := json.Marshal(jwk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot marshal jwk: %w", err)
|
||||
}
|
||||
|
||||
jwks := jose.JSONWebKeySet{
|
||||
Keys: []jose.JSONWebKey{jwk.Public()},
|
||||
}
|
||||
jwksData, err := json.Marshal(jwks)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot marshal jwks: %w", err)
|
||||
}
|
||||
|
||||
s := corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: opc.Name + "-jwks",
|
||||
Namespace: opc.Namespace,
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
*metav1.NewControllerRef(opc, schema.GroupVersionKind{
|
||||
Group: configv1alpha1.SchemeGroupVersion.Group,
|
||||
Version: configv1alpha1.SchemeGroupVersion.Version,
|
||||
Kind: opcKind,
|
||||
}),
|
||||
},
|
||||
// TODO: custom labels.
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
activeJWKKey: jwkData,
|
||||
jwksKey: jwksData,
|
||||
},
|
||||
}
|
||||
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (c *jwksController) createOrUpdateSecret(
|
||||
ctx context.Context,
|
||||
newSecret *corev1.Secret,
|
||||
) error {
|
||||
secretClient := c.kubeClient.CoreV1().Secrets(newSecret.Namespace)
|
||||
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
oldSecret, err := secretClient.Get(ctx, newSecret.Name, metav1.GetOptions{})
|
||||
notFound := k8serrors.IsNotFound(err)
|
||||
if err != nil && !notFound {
|
||||
return fmt.Errorf("cannot get secret: %w", err)
|
||||
}
|
||||
|
||||
if notFound {
|
||||
// New secret doesn't exist, so create it.
|
||||
_, err := secretClient.Create(ctx, newSecret, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create secret: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// New secret already exists, so ensure it is up to date.
|
||||
|
||||
if isValid(oldSecret) {
|
||||
// If the secret already has valid JWK's, then we are good to go and we don't need an update.
|
||||
return nil
|
||||
}
|
||||
|
||||
oldSecret.Data = newSecret.Data
|
||||
_, err = secretClient.Update(ctx, oldSecret, metav1.UpdateOptions{})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (c *jwksController) updateOPC(
|
||||
ctx context.Context,
|
||||
newOPC *configv1alpha1.OIDCProviderConfig,
|
||||
) error {
|
||||
opcClient := c.pinnipedClient.ConfigV1alpha1().OIDCProviderConfigs(newOPC.Namespace)
|
||||
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
oldOPC, err := opcClient.Get(ctx, newOPC.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get opc: %w", err)
|
||||
}
|
||||
|
||||
if newOPC.Status.JWKSSecret.Name == oldOPC.Status.JWKSSecret.Name {
|
||||
// If the existing OPC is up to date, we don't need to update it.
|
||||
return nil
|
||||
}
|
||||
|
||||
oldOPC.Status.JWKSSecret.Name = newOPC.Status.JWKSSecret.Name
|
||||
_, err = opcClient.Update(ctx, oldOPC, metav1.UpdateOptions{})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// isOPCControlle returns whether the provided obj is controlled by an OPC.
|
||||
func isOPCControllee(obj metav1.Object) bool {
|
||||
controller := metav1.GetControllerOf(obj)
|
||||
return controller != nil &&
|
||||
controller.APIVersion == configv1alpha1.SchemeGroupVersion.String() &&
|
||||
controller.Kind == opcKind
|
||||
}
|
||||
|
||||
// isValid returns whether the provided secret contains a valid active JWK and verification JWKS.
|
||||
func isValid(secret *corev1.Secret) bool {
|
||||
jwkData, ok := secret.Data[activeJWKKey]
|
||||
if !ok {
|
||||
klog.InfoS("secret does not contain active jwk")
|
||||
return false
|
||||
}
|
||||
|
||||
var activeJWK jose.JSONWebKey
|
||||
if err := json.Unmarshal(jwkData, &activeJWK); err != nil {
|
||||
klog.InfoS("cannot unmarshal active jwk", "err", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if activeJWK.IsPublic() {
|
||||
klog.InfoS("active jwk is public", "keyid", activeJWK.KeyID)
|
||||
return false
|
||||
}
|
||||
|
||||
if !activeJWK.Valid() {
|
||||
klog.InfoS("active jwk is not valid", "keyid", activeJWK.KeyID)
|
||||
return false
|
||||
}
|
||||
|
||||
jwksData, ok := secret.Data[jwksKey]
|
||||
if !ok {
|
||||
klog.InfoS("secret does not contain valid jwks")
|
||||
}
|
||||
|
||||
var validJWKS jose.JSONWebKeySet
|
||||
if err := json.Unmarshal(jwksData, &validJWKS); err != nil {
|
||||
klog.InfoS("cannot unmarshal valid jwks", "err", err)
|
||||
return false
|
||||
}
|
||||
|
||||
foundActiveJWK := false
|
||||
for _, validJWK := range validJWKS.Keys {
|
||||
if !validJWK.IsPublic() {
|
||||
klog.InfoS("jwks key is not public", "keyid", validJWK.KeyID)
|
||||
return false
|
||||
}
|
||||
if !validJWK.Valid() {
|
||||
klog.InfoS("jwks key is not valid", "keyid", validJWK.KeyID)
|
||||
return false
|
||||
}
|
||||
if validJWK.KeyID == activeJWK.KeyID {
|
||||
foundActiveJWK = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundActiveJWK {
|
||||
klog.InfoS("did not find active jwk in valid jwks", "keyid", activeJWK.KeyID)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,699 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package supervisorconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
||||
kubetesting "k8s.io/client-go/testing"
|
||||
|
||||
configv1alpha1 "go.pinniped.dev/generated/1.19/apis/config/v1alpha1"
|
||||
pinnipedfake "go.pinniped.dev/generated/1.19/client/clientset/versioned/fake"
|
||||
pinnipedinformers "go.pinniped.dev/generated/1.19/client/informers/externalversions"
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
func TestJWKSControllerFilterSecret(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
secret corev1.Secret
|
||||
wantAdd bool
|
||||
wantUpdate bool
|
||||
wantDelete bool
|
||||
wantParent controllerlib.Key
|
||||
}{
|
||||
{
|
||||
name: "no owner reference",
|
||||
secret: corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner reference without correct APIVersion",
|
||||
secret: corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "some-namespace",
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
Kind: "OIDCProviderConfig",
|
||||
Name: "some-name",
|
||||
Controller: boolPtr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner reference without correct Kind",
|
||||
secret: corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "some-namespace",
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
|
||||
Name: "some-name",
|
||||
Controller: boolPtr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner reference without controller set to true",
|
||||
secret: corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "some-namespace",
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: "OIDCProviderConfig",
|
||||
Name: "some-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "correct owner reference",
|
||||
secret: corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "some-namespace",
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: "OIDCProviderConfig",
|
||||
Name: "some-name",
|
||||
Controller: boolPtr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantAdd: true,
|
||||
wantUpdate: true,
|
||||
wantDelete: true,
|
||||
wantParent: controllerlib.Key{Namespace: "some-namespace", Name: "some-name"},
|
||||
},
|
||||
{
|
||||
name: "multiple owner references",
|
||||
secret: corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "some-namespace",
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
Kind: "UnrelatedKind",
|
||||
},
|
||||
{
|
||||
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: "OIDCProviderConfig",
|
||||
Name: "some-name",
|
||||
Controller: boolPtr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantAdd: true,
|
||||
wantUpdate: true,
|
||||
wantDelete: true,
|
||||
wantParent: controllerlib.Key{Namespace: "some-namespace", Name: "some-name"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
secretInformer := kubeinformers.NewSharedInformerFactory(
|
||||
kubernetesfake.NewSimpleClientset(),
|
||||
0,
|
||||
).Core().V1().Secrets()
|
||||
opcInformer := pinnipedinformers.NewSharedInformerFactory(
|
||||
pinnipedfake.NewSimpleClientset(),
|
||||
0,
|
||||
).Config().V1alpha1().OIDCProviderConfigs()
|
||||
withInformer := testutil.NewObservableWithInformerOption()
|
||||
_ = NewJWKSController(
|
||||
nil, // kubeClient, not needed
|
||||
nil, // pinnipedClient, not needed
|
||||
secretInformer,
|
||||
opcInformer,
|
||||
withInformer.WithInformer,
|
||||
)
|
||||
|
||||
unrelated := corev1.Secret{}
|
||||
filter := withInformer.GetFilterForInformer(secretInformer)
|
||||
require.Equal(t, test.wantAdd, filter.Add(&test.secret))
|
||||
require.Equal(t, test.wantUpdate, filter.Update(&unrelated, &test.secret))
|
||||
require.Equal(t, test.wantUpdate, filter.Update(&test.secret, &unrelated))
|
||||
require.Equal(t, test.wantDelete, filter.Delete(&test.secret))
|
||||
require.Equal(t, test.wantParent, filter.Parent(&test.secret))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWKSControllerFilterOPC(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opc configv1alpha1.OIDCProviderConfig
|
||||
wantAdd bool
|
||||
wantUpdate bool
|
||||
wantDelete bool
|
||||
wantParent controllerlib.Key
|
||||
}{
|
||||
{
|
||||
name: "anything goes",
|
||||
opc: configv1alpha1.OIDCProviderConfig{},
|
||||
wantAdd: true,
|
||||
wantUpdate: true,
|
||||
wantDelete: true,
|
||||
wantParent: controllerlib.Key{},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
secretInformer := kubeinformers.NewSharedInformerFactory(
|
||||
kubernetesfake.NewSimpleClientset(),
|
||||
0,
|
||||
).Core().V1().Secrets()
|
||||
opcInformer := pinnipedinformers.NewSharedInformerFactory(
|
||||
pinnipedfake.NewSimpleClientset(),
|
||||
0,
|
||||
).Config().V1alpha1().OIDCProviderConfigs()
|
||||
withInformer := testutil.NewObservableWithInformerOption()
|
||||
_ = NewJWKSController(
|
||||
nil, // kubeClient, not needed
|
||||
nil, // pinnipedClient, not needed
|
||||
secretInformer,
|
||||
opcInformer,
|
||||
withInformer.WithInformer,
|
||||
)
|
||||
|
||||
unrelated := configv1alpha1.OIDCProviderConfig{}
|
||||
filter := withInformer.GetFilterForInformer(opcInformer)
|
||||
require.Equal(t, test.wantAdd, filter.Add(&test.opc))
|
||||
require.Equal(t, test.wantUpdate, filter.Update(&unrelated, &test.opc))
|
||||
require.Equal(t, test.wantUpdate, filter.Update(&test.opc, &unrelated))
|
||||
require.Equal(t, test.wantDelete, filter.Delete(&test.opc))
|
||||
require.Equal(t, test.wantParent, filter.Parent(&test.opc))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWKSControllerSync(t *testing.T) {
|
||||
// We shouldn't run this test in parallel since it messes with a global function (generateKey).
|
||||
|
||||
const namespace = "tuna-namespace"
|
||||
|
||||
goodKeyPEM, err := ioutil.ReadFile("testdata/good-ec-key.pem")
|
||||
require.NoError(t, err)
|
||||
block, _ := pem.Decode(goodKeyPEM)
|
||||
require.NotNil(t, block, "expected block to be non-nil...is goodKeyPEM a valid PEM?")
|
||||
goodKey, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
opcGVR := schema.GroupVersionResource{
|
||||
Group: configv1alpha1.SchemeGroupVersion.Group,
|
||||
Version: configv1alpha1.SchemeGroupVersion.Version,
|
||||
Resource: "oidcproviderconfigs",
|
||||
}
|
||||
|
||||
goodOPC := &configv1alpha1.OIDCProviderConfig{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "good-opc",
|
||||
Namespace: namespace,
|
||||
UID: "good-opc-uid",
|
||||
},
|
||||
Spec: configv1alpha1.OIDCProviderConfigSpec{
|
||||
Issuer: "https://some-issuer.com",
|
||||
},
|
||||
}
|
||||
goodOPCWithStatus := goodOPC.DeepCopy()
|
||||
goodOPCWithStatus.Status.JWKSSecret.Name = goodOPCWithStatus.Name + "-jwks"
|
||||
|
||||
secretGVR := schema.GroupVersionResource{
|
||||
Group: corev1.SchemeGroupVersion.Group,
|
||||
Version: corev1.SchemeGroupVersion.Version,
|
||||
Resource: "secrets",
|
||||
}
|
||||
|
||||
newSecret := func(activeJWKPath, jwksPath string) *corev1.Secret {
|
||||
s := corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: goodOPCWithStatus.Status.JWKSSecret.Name,
|
||||
Namespace: namespace,
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: opcGVR.GroupVersion().String(),
|
||||
Kind: "OIDCProviderConfig",
|
||||
Name: goodOPC.Name,
|
||||
UID: goodOPC.UID,
|
||||
BlockOwnerDeletion: boolPtr(true),
|
||||
Controller: boolPtr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
s.Data = make(map[string][]byte)
|
||||
if activeJWKPath != "" {
|
||||
s.Data["activeJWK"] = read(t, activeJWKPath)
|
||||
}
|
||||
if jwksPath != "" {
|
||||
s.Data["jwks"] = read(t, jwksPath)
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
goodSecret := newSecret("testdata/good-jwk.json", "testdata/good-jwks.json")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key controllerlib.Key
|
||||
secrets []*corev1.Secret
|
||||
configKubeClient func(*kubernetesfake.Clientset)
|
||||
configPinnipedClient func(*pinnipedfake.Clientset)
|
||||
opcs []*configv1alpha1.OIDCProviderConfig
|
||||
generateKeyErr error
|
||||
wantGenerateKeyCount int
|
||||
wantSecretActions []kubetesting.Action
|
||||
wantOPCActions []kubetesting.Action
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "new opc with no secret",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPC,
|
||||
},
|
||||
wantGenerateKeyCount: 1,
|
||||
wantSecretActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
||||
kubetesting.NewCreateAction(secretGVR, namespace, goodSecret),
|
||||
},
|
||||
wantOPCActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name),
|
||||
kubetesting.NewUpdateAction(opcGVR, namespace, goodOPCWithStatus),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "opc without status with existing secret",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPC,
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
goodSecret,
|
||||
},
|
||||
wantGenerateKeyCount: 1,
|
||||
wantSecretActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
||||
},
|
||||
wantOPCActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name),
|
||||
kubetesting.NewUpdateAction(opcGVR, namespace, goodOPCWithStatus),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing opc with no secret",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPCWithStatus,
|
||||
},
|
||||
wantGenerateKeyCount: 1,
|
||||
wantSecretActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
||||
kubetesting.NewCreateAction(secretGVR, namespace, goodSecret),
|
||||
},
|
||||
wantOPCActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing opc with existing secret",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPCWithStatus,
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
goodSecret,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deleted opc",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
// Nothing to do here since Kube will garbage collect our child secret via its OwnerReference.
|
||||
},
|
||||
{
|
||||
name: "missing jwk in secret",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPCWithStatus,
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
newSecret("", "testdata/good-jwks.json"),
|
||||
},
|
||||
wantGenerateKeyCount: 1,
|
||||
wantSecretActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
||||
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
||||
},
|
||||
wantOPCActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing jwks in secret",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPCWithStatus,
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
newSecret("testdata/good-jwk.json", ""),
|
||||
},
|
||||
wantGenerateKeyCount: 1,
|
||||
wantSecretActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
||||
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
||||
},
|
||||
wantOPCActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid jwk JSON in secret",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPCWithStatus,
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
newSecret("testdata/not-json.txt", "testdata/good-jwks.json"),
|
||||
},
|
||||
wantGenerateKeyCount: 1,
|
||||
wantSecretActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
||||
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
||||
},
|
||||
wantOPCActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid jwks JSON in secret",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPCWithStatus,
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
newSecret("testdata/good-jwk.json", "testdata/not-json.txt"),
|
||||
},
|
||||
wantGenerateKeyCount: 1,
|
||||
wantSecretActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
||||
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
||||
},
|
||||
wantOPCActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "public jwk in secret",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPCWithStatus,
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
newSecret("testdata/public-jwk.json", "testdata/good-jwks.json"),
|
||||
},
|
||||
wantGenerateKeyCount: 1,
|
||||
wantSecretActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
||||
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
||||
},
|
||||
wantOPCActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "private jwks in secret",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPCWithStatus,
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
newSecret("testdata/good-jwk.json", "testdata/private-jwks.json"),
|
||||
},
|
||||
wantGenerateKeyCount: 1,
|
||||
wantSecretActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
||||
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
||||
},
|
||||
wantOPCActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid jwk key in secret",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPCWithStatus,
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
newSecret("testdata/invalid-key-jwk.json", "testdata/good-jwks.json"),
|
||||
},
|
||||
wantGenerateKeyCount: 1,
|
||||
wantSecretActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
||||
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
||||
},
|
||||
wantOPCActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid jwks key in secret",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPCWithStatus,
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
newSecret("testdata/good-jwk.json", "testdata/invalid-key-jwks.json"),
|
||||
},
|
||||
wantGenerateKeyCount: 1,
|
||||
wantSecretActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
||||
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
||||
},
|
||||
wantOPCActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing active jwks in secret",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPCWithStatus,
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
newSecret("testdata/good-jwk.json", "testdata/missing-active-jwks.json"),
|
||||
},
|
||||
wantGenerateKeyCount: 1,
|
||||
wantSecretActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
||||
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
||||
},
|
||||
wantOPCActions: []kubetesting.Action{
|
||||
kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "generate key fails",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPCWithStatus,
|
||||
},
|
||||
generateKeyErr: errors.New("some generate error"),
|
||||
wantError: "cannot generate secret: cannot generate key: some generate error",
|
||||
},
|
||||
{
|
||||
name: "get secret fails",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPC,
|
||||
},
|
||||
configKubeClient: func(client *kubernetesfake.Clientset) {
|
||||
client.PrependReactor("get", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, errors.New("some get error")
|
||||
})
|
||||
},
|
||||
wantError: "cannot create or update secret: cannot get secret: some get error",
|
||||
},
|
||||
{
|
||||
name: "create secret fails",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPC,
|
||||
},
|
||||
configKubeClient: func(client *kubernetesfake.Clientset) {
|
||||
client.PrependReactor("create", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, errors.New("some create error")
|
||||
})
|
||||
},
|
||||
wantError: "cannot create or update secret: cannot create secret: some create error",
|
||||
},
|
||||
{
|
||||
name: "update secret fails",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPC,
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
newSecret("", ""),
|
||||
},
|
||||
configKubeClient: func(client *kubernetesfake.Clientset) {
|
||||
client.PrependReactor("update", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, errors.New("some update error")
|
||||
})
|
||||
},
|
||||
wantError: "cannot create or update secret: some update error",
|
||||
},
|
||||
{
|
||||
name: "get opc fails",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPC,
|
||||
},
|
||||
configPinnipedClient: func(client *pinnipedfake.Clientset) {
|
||||
client.PrependReactor("get", "oidcproviderconfigs", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, errors.New("some get error")
|
||||
})
|
||||
},
|
||||
wantError: "cannot update opc: cannot get opc: some get error",
|
||||
},
|
||||
{
|
||||
name: "update opc fails",
|
||||
key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name},
|
||||
opcs: []*configv1alpha1.OIDCProviderConfig{
|
||||
goodOPC,
|
||||
},
|
||||
configPinnipedClient: func(client *pinnipedfake.Clientset) {
|
||||
client.PrependReactor("update", "oidcproviderconfigs", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, errors.New("some update error")
|
||||
})
|
||||
},
|
||||
wantError: "cannot update opc: some update error",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// We shouldn't run this test in parallel since it messes with a global function (generateKey).
|
||||
generateKeyCount := 0
|
||||
generateKey = func(_ io.Reader) (interface{}, error) {
|
||||
generateKeyCount++
|
||||
return goodKey, test.generateKeyErr
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||
defer cancel()
|
||||
|
||||
kubeAPIClient := kubernetesfake.NewSimpleClientset()
|
||||
kubeInformerClient := kubernetesfake.NewSimpleClientset()
|
||||
for _, secret := range test.secrets {
|
||||
require.NoError(t, kubeAPIClient.Tracker().Add(secret))
|
||||
require.NoError(t, kubeInformerClient.Tracker().Add(secret))
|
||||
}
|
||||
if test.configKubeClient != nil {
|
||||
test.configKubeClient(kubeAPIClient)
|
||||
}
|
||||
|
||||
pinnipedAPIClient := pinnipedfake.NewSimpleClientset()
|
||||
pinnipedInformerClient := pinnipedfake.NewSimpleClientset()
|
||||
for _, opc := range test.opcs {
|
||||
require.NoError(t, pinnipedAPIClient.Tracker().Add(opc))
|
||||
require.NoError(t, pinnipedInformerClient.Tracker().Add(opc))
|
||||
}
|
||||
if test.configPinnipedClient != nil {
|
||||
test.configPinnipedClient(pinnipedAPIClient)
|
||||
}
|
||||
|
||||
kubeInformers := kubeinformers.NewSharedInformerFactory(
|
||||
kubeInformerClient,
|
||||
0,
|
||||
)
|
||||
pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(
|
||||
pinnipedInformerClient,
|
||||
0,
|
||||
)
|
||||
|
||||
c := NewJWKSController(
|
||||
kubeAPIClient,
|
||||
pinnipedAPIClient,
|
||||
kubeInformers.Core().V1().Secrets(),
|
||||
pinnipedInformers.Config().V1alpha1().OIDCProviderConfigs(),
|
||||
controllerlib.WithInformer,
|
||||
)
|
||||
|
||||
// Must start informers before calling TestRunSynchronously().
|
||||
kubeInformers.Start(ctx.Done())
|
||||
pinnipedInformers.Start(ctx.Done())
|
||||
controllerlib.TestRunSynchronously(t, c)
|
||||
|
||||
err := controllerlib.TestSync(t, c, controllerlib.Context{
|
||||
Context: ctx,
|
||||
Key: test.key,
|
||||
})
|
||||
if test.wantError != "" {
|
||||
require.EqualError(t, err, test.wantError)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, test.wantGenerateKeyCount, generateKeyCount)
|
||||
|
||||
if test.wantSecretActions != nil {
|
||||
require.Equal(t, test.wantSecretActions, kubeAPIClient.Actions())
|
||||
}
|
||||
if test.wantOPCActions != nil {
|
||||
require.Equal(t, test.wantOPCActions, pinnipedAPIClient.Actions())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func read(t *testing.T, path string) []byte {
|
||||
t.Helper()
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Trim whitespace from our testdata so that we match the compact JSON encoding of
|
||||
// our implementation.
|
||||
data = bytes.ReplaceAll(data, []byte(" "), []byte{})
|
||||
data = bytes.ReplaceAll(data, []byte("\n"), []byte{})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
@@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEINR2PAduYBO64CaDT4vLoMnn8y4UX5VTFdOA7wUQF0n/oAoGCCqGSM49
|
||||
AwEHoUQDQgAEawmmj6CIMhSoJyfsqH7sekbTeY72GGPLEy16tPWVz2UVwyHTq5ct
|
||||
qr1vYw6LGUtWJ1STJw7W7sgc6StOLs3RrA==
|
||||
-----END EC PRIVATE KEY-----
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"use": "sig",
|
||||
"kty": "EC",
|
||||
"kid": "pinniped-supervisor-key",
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"x": "awmmj6CIMhSoJyfsqH7sekbTeY72GGPLEy16tPWVz2U",
|
||||
"y": "FcMh06uXLaq9b2MOixlLVidUkycO1u7IHOkrTi7N0aw",
|
||||
"d": "1HY8B25gE7rgJoNPi8ugyefzLhRflVMV04DvBRAXSf8"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"use": "sig",
|
||||
"kty": "EC",
|
||||
"kid": "pinniped-supervisor-key",
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"x": "awmmj6CIMhSoJyfsqH7sekbTeY72GGPLEy16tPWVz2U",
|
||||
"y": "FcMh06uXLaq9b2MOixlLVidUkycO1u7IHOkrTi7N0aw"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"use": "sig",
|
||||
"kty": "EC",
|
||||
"kid": "pinniped-supervisor-key",
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"x": "0",
|
||||
"y": "FcMh06uXLaq9b2MOixlLVidUkycO1u7IHOkrTi7N0aw",
|
||||
"d": "1HY8B25gE7rgJoNPi8ugyefzLhRflVMV04DvBRAXSf8"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"use": "sig",
|
||||
"kty": "EC",
|
||||
"kid": "pinniped-supervisor-key",
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"x": "awmmj6CIMhSoJyfsqH7sekbTeY72GGPLEy16tPWVz2U",
|
||||
"y": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"use": "sig",
|
||||
"kty": "EC",
|
||||
"kid": "some-other-key",
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"x": "awmmj6CIMhSoJyfsqH7sekbTeY72GGPLEy16tPWVz2U",
|
||||
"y": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
this ain't json
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"use": "sig",
|
||||
"kty": "EC",
|
||||
"kid": "pinniped-supervisor-key",
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"x": "awmmj6CIMhSoJyfsqH7sekbTeY72GGPLEy16tPWVz2U",
|
||||
"y": "FcMh06uXLaq9b2MOixlLVidUkycO1u7IHOkrTi7N0aw",
|
||||
"d": "1HY8B25gE7rgJoNPi8ugyefzLhRflVMV04DvBRAXSf8"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"use": "sig",
|
||||
"kty": "EC",
|
||||
"kid": "pinniped-supervisor-key",
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"x": "awmmj6CIMhSoJyfsqH7sekbTeY72GGPLEy16tPWVz2U",
|
||||
"y": "FcMh06uXLaq9b2MOixlLVidUkycO1u7IHOkrTi7N0aw"
|
||||
}
|
||||
Reference in New Issue
Block a user