oidc

package
v0.0.0-...-8caed39 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: OSL-3.0 Imports: 27 Imported by: 0

Documentation

Overview

Package oidc implements the CUDly OIDC issuer used to federate into target clouds (currently Azure) without storing any long-lived secret on the CUDly side.

The package exposes:

  • Signer: a cloud-agnostic interface for producing ECDSA (ES256) signatures over a SHA-256 digest. Backed by AWS KMS, Azure Key Vault, or GCP Cloud KMS depending on where CUDly runs. The private key never leaves the cloud KMS.

  • Mint: a helper that takes a Signer and a set of JWT claims and produces a compact JWS (header.payload.signature) suitable for use as an OAuth 2.0 client_assertion when the target cloud has been configured with a federated identity credential pointing at CUDly's OIDC issuer.

  • LocalSigner: an in-process ECDSA signer used only by tests.

Index

Constants

View Source
const Algorithm = "ES256"

Algorithm is the JWS algorithm used throughout the package. All three backends (AWS KMS, Azure Key Vault, GCP Cloud KMS) support ES256 over a P-256 key, which is also what Azure AD accepts for a federated identity credential's client_assertion.

Variables

This section is empty.

Functions

func ComputeKeyID

func ComputeKeyID(pub crypto.PublicKey) (string, error)

ComputeKeyID returns a stable kid for a public key. For *ecdsa.PublicKey: SHA-256 of the uncompressed point (0x04 || X || Y), base64url-encoded without padding. Stable across restarts, new on every key rotation.

func IssuerURL

func IssuerURL() string

IssuerURL returns whatever was last stored via SetIssuerURL, or the empty string if none was.

func Mint

func Mint(ctx context.Context, signer Signer, claims map[string]any) (string, error)

Mint produces a compact JWS signed by the given Signer. claims is serialized as the JWT payload; the header is constructed from the Signer's key id plus the ES256 algorithm. The Signer.Sign contract returns the raw R || S signature (RFC 7518 section 3.4), so Mint base64url-encodes it directly into the JWS signature segment.

func PrimeIssuerURLFromLambda

func PrimeIssuerURLFromLambda(ctx context.Context) error

PrimeIssuerURLFromLambda looks up the running Lambda's own Function URL via lambda:GetFunctionUrlConfig and stores <function-url-without-trailing-slash> + OIDCBasePath in the package cache. This closes the cold-start race where a scheduled task (or any code path that doesn't go through the HTTP handler first) triggers a credential exchange before the cache has been populated from an inbound request.

No-op (and returns nil) when not running in Lambda (AWS_LAMBDA_FUNCTION_NAME is empty). Errors are returned to the caller so startup code can log them, but they should not fail cold start — the request-driven population path is still a backstop.

func SetIssuerURL

func SetIssuerURL(rawURL string) error

SetIssuerURL stores the deployment's OIDC issuer URL. The URL must be an absolute https:// URL; non-https or relative URLs are rejected and the cache is left unchanged (03-L4). No-op when called with an empty string.

Types

type AWSKMSClient

type AWSKMSClient interface {
	Sign(ctx context.Context, params *kms.SignInput, optFns ...func(*kms.Options)) (*kms.SignOutput, error)
	GetPublicKey(ctx context.Context, params *kms.GetPublicKeyInput, optFns ...func(*kms.Options)) (*kms.GetPublicKeyOutput, error)
}

AWSKMSClient is the subset of the AWS KMS API that the signer needs. Exposed as an interface so tests can pass a fake client.

type AWSKMSSigner

type AWSKMSSigner struct {
	// contains filtered or unexported fields
}

AWSKMSSigner signs JWTs by delegating to an AWS KMS asymmetric key. The private key never leaves KMS.

func NewAWSKMSSigner

func NewAWSKMSSigner(ctx context.Context, keyID string) (*AWSKMSSigner, error)

NewAWSKMSSigner constructs a signer bound to the given KMS key. The keyID may be the key ARN, alias ARN, or alias name -- anything accepted by kms:Sign and kms:GetPublicKey. The KMS key must be an ECC_NIST_P256 key configured for signing (ES256).

func NewAWSKMSSignerFromClient

func NewAWSKMSSignerFromClient(client AWSKMSClient, keyID string) *AWSKMSSigner

NewAWSKMSSignerFromClient lets callers (mostly tests) inject a fake KMS client. The public key is still fetched lazily via the client.

func (*AWSKMSSigner) KeyID

func (s *AWSKMSSigner) KeyID(ctx context.Context) (string, error)

KeyID returns the JWK kid derived from the cached public key.

func (*AWSKMSSigner) PublicKey

func (s *AWSKMSSigner) PublicKey(ctx context.Context) (crypto.PublicKey, error)

PublicKey fetches the public half of the KMS key once and caches it. Subsequent calls return the cached value.

func (*AWSKMSSigner) Sign

func (s *AWSKMSSigner) Sign(ctx context.Context, digest []byte) ([]byte, error)

Sign calls kms:Sign with the raw SHA-256 digest and the signing algorithm ECDSA_SHA_256. AWS KMS returns a DER/ASN.1-encoded ECDSA signature, which is converted to the RFC 7518 section 3.4 raw R || S form required by ES256 JWS signatures (the Signer.Sign contract).

type AzureKeyVaultClient

type AzureKeyVaultClient interface {
	Sign(ctx context.Context, name, version string, parameters azkeys.SignParameters, options *azkeys.SignOptions) (azkeys.SignResponse, error)
	GetKey(ctx context.Context, name, version string, options *azkeys.GetKeyOptions) (azkeys.GetKeyResponse, error)
}

AzureKeyVaultClient is the subset of the azkeys API the signer needs. Defined as an interface so tests can swap in a fake.

type AzureKeyVaultSigner

type AzureKeyVaultSigner struct {
	// contains filtered or unexported fields
}

AzureKeyVaultSigner signs JWTs via an Azure Key Vault EC key (P-256). The private half never leaves the vault.

func NewAzureKeyVaultSigner

func NewAzureKeyVaultSigner(ctx context.Context, vaultURL, keyName string) (*AzureKeyVaultSigner, error)

NewAzureKeyVaultSigner constructs a signer against a Key Vault using the standard azidentity default credential chain. vaultURL is the full vault URL (e.g. https://cudly-vault.vault.azure.net/); keyName is the name of the EC (P-256) key in that vault.

func NewAzureKeyVaultSignerFromClient

func NewAzureKeyVaultSignerFromClient(client AzureKeyVaultClient, keyName, keyVersion string) *AzureKeyVaultSigner

NewAzureKeyVaultSignerFromClient constructs a signer with an explicit client. Used by tests. keyVersion may be empty to select the current version.

func (*AzureKeyVaultSigner) KeyID

func (s *AzureKeyVaultSigner) KeyID(ctx context.Context) (string, error)

KeyID returns a stable kid derived from the public key point.

func (*AzureKeyVaultSigner) PublicKey

PublicKey fetches the public half of the Key Vault key once and caches it.

func (*AzureKeyVaultSigner) Sign

func (s *AzureKeyVaultSigner) Sign(ctx context.Context, digest []byte) ([]byte, error)

Sign calls Key Vault's Sign operation with ES256, passing the raw SHA-256 digest. Unlike AWS KMS and GCP Cloud KMS (which return DER), Azure Key Vault's ES256 already returns the IEEE P1363 / RFC 7518 raw R || S signature (64 bytes for P-256), so it satisfies the Signer.Sign contract directly and must NOT be DER-converted.

type Discovery

type Discovery struct {
	Issuer                           string   `json:"issuer"`
	JWKSURI                          string   `json:"jwks_uri"`
	ResponseTypesSupported           []string `json:"response_types_supported"`
	SubjectTypesSupported            []string `json:"subject_types_supported"`
	IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
}

Discovery is the subset of the OpenID Connect Discovery document (RFC 8414) that Azure AD actually consults when validating a federated identity credential client_assertion.

CUDly is not a full OIDC provider — it never issues ID tokens and never runs the authorization or token endpoints. It only publishes enough of a discovery document for Azure AD to locate the JWKS.

func BuildDiscovery

func BuildDiscovery(issuer string) Discovery

BuildDiscovery returns the discovery document for the given issuer URL. issuer must match exactly what the Azure AD federated identity credential is configured with (trailing slashes included or excluded consistently), otherwise token validation fails with AADSTS700213.

type GCPKMSClient

type GCPKMSClient interface {
	AsymmetricSign(ctx context.Context, req *kmspb.AsymmetricSignRequest, opts ...interface{}) (*kmspb.AsymmetricSignResponse, error)
	GetPublicKey(ctx context.Context, req *kmspb.GetPublicKeyRequest, opts ...interface{}) (*kmspb.PublicKey, error)
}

GCPKMSClient is the subset of the GCP Cloud KMS API the signer needs.

type GCPKMSSigner

type GCPKMSSigner struct {
	// contains filtered or unexported fields
}

GCPKMSSigner signs JWTs using a GCP Cloud KMS asymmetric key (EC P-256). The private half never leaves the KMS.

func NewGCPKMSSigner

func NewGCPKMSSigner(ctx context.Context, keyResource string) (*GCPKMSSigner, error)

NewGCPKMSSigner constructs a signer bound to a specific KMS key version resource. Example resource:

projects/.../locations/global/keyRings/.../cryptoKeys/.../cryptoKeyVersions/1

The key must be an EC_SIGN_P256_SHA256 key.

func NewGCPKMSSignerFromClient

func NewGCPKMSSignerFromClient(client GCPKMSClient, keyResource string) *GCPKMSSigner

NewGCPKMSSignerFromClient lets callers inject a fake client (tests).

func (*GCPKMSSigner) KeyID

func (s *GCPKMSSigner) KeyID(ctx context.Context) (string, error)

KeyID returns a stable kid derived from the cached public key.

func (*GCPKMSSigner) PublicKey

func (s *GCPKMSSigner) PublicKey(ctx context.Context) (crypto.PublicKey, error)

PublicKey fetches the public half of the KMS key once and caches it.

func (*GCPKMSSigner) Sign

func (s *GCPKMSSigner) Sign(ctx context.Context, digest []byte) ([]byte, error)

Sign calls AsymmetricSign with the SHA-256 digest. The caller must have already hashed the signing input; the digest is forwarded as-is. The key must be configured as EC_SIGN_P256_SHA256 in GCP KMS. GCP Cloud KMS returns a DER/ASN.1-encoded ECDSA signature, which is converted to the RFC 7518 section 3.4 raw R || S form required by ES256 JWS signatures (the Signer.Sign contract).

type JWK

type JWK struct {
	Kty string   `json:"kty"`           // "EC" for ECDSA keys
	Use string   `json:"use"`           // always "sig"
	Alg string   `json:"alg"`           // always "ES256"
	Kid string   `json:"kid"`           // stable key id
	Crv string   `json:"crv"`           // "P-256"
	X   string   `json:"x"`             // base64url x-coordinate
	Y   string   `json:"y"`             // base64url y-coordinate
	X5c []string `json:"x5c,omitempty"` // certificate chain (unused)
}

JWK is a minimal RFC 7517 JSON Web Key for a public EC signing key. Only the fields needed by Azure AD federated credential validation are serialized. RSA fields (N, E) are omitted; EC fields (Crv, X, Y) carry the P-256 public point per RFC 7518 §6.2.

func PublicJWK

func PublicJWK(pub crypto.PublicKey, kid string) (JWK, error)

PublicJWK derives a JWK from an ECDSA public key and a kid. Only P-256 keys (matching ES256) are accepted.

type JWKS

type JWKS struct {
	Keys []JWK `json:"keys"`
}

JWKS is the container returned by /.well-known/jwks.json.

func BuildJWKS

func BuildJWKS(ctx context.Context, signer Signer) (JWKS, error)

BuildJWKS wraps a single Signer into a JWKS document. The set has exactly one key today; rotation-with-overlap is left as future work per specs/azure-wif-redesign.md.

type LambdaFunctionURLClient

type LambdaFunctionURLClient interface {
	GetFunctionUrlConfig(ctx context.Context, params *lambda.GetFunctionUrlConfigInput, optFns ...func(*lambda.Options)) (*lambda.GetFunctionUrlConfigOutput, error)
}

LambdaFunctionURLClient is the subset of the AWS Lambda API the issuer-cache primer needs. Exposed as an interface so tests can inject a fake without dialing AWS.

type LocalSigner

type LocalSigner struct {
	// contains filtered or unexported fields
}

LocalSigner is an in-process ECDSA signer used only by tests. Callers must NOT use it outside of test code; real deployments must back the Signer interface with a cloud KMS so the private key never hits the CUDly process.

func NewLocalSigner

func NewLocalSigner() (*LocalSigner, error)

NewLocalSigner generates a new P-256 ECDSA key and wraps it in a test-only Signer. The returned kid is a hash-based identifier stable across calls for the same key.

func (*LocalSigner) KeyID

func (s *LocalSigner) KeyID(_ context.Context) (string, error)

KeyID returns the stable identifier for this local signer.

func (*LocalSigner) PublicKey

func (s *LocalSigner) PublicKey(_ context.Context) (crypto.PublicKey, error)

PublicKey returns the test key's public half.

func (*LocalSigner) Sign

func (s *LocalSigner) Sign(_ context.Context, digest []byte) ([]byte, error)

Sign signs digest with the test ECDSA P-256 key. crypto/ecdsa.SignASN1 produces a DER/ASN.1 signature (the same format cloud KMS ECDSA operations return), which is converted to the RFC 7518 raw R || S form to satisfy the Signer.Sign contract.

type Signer

type Signer interface {
	// Sign returns the raw fixed-length ECDSA signature over digest in the
	// RFC 7518 section 3.4 JWS form: the R || S concatenation, each
	// component left-padded to 32 bytes (64 bytes total for P-256). The
	// caller is responsible for hashing the input. Backends whose KMS
	// returns DER/ASN.1 (AWS, GCP, and the in-process LocalSigner) MUST
	// convert via derToRawECDSASignature before returning; Azure Key Vault
	// already returns this raw form and passes it through unchanged. This
	// lets Mint base64url-encode the result directly into the JWS without
	// any per-backend special-casing.
	Sign(ctx context.Context, digest []byte) ([]byte, error)

	// PublicKey returns the public key corresponding to the signer.
	// Cached after the first call per implementation. Returns
	// *ecdsa.PublicKey for ES256 signers.
	PublicKey(ctx context.Context) (crypto.PublicKey, error)

	// KeyID returns a stable identifier used as the JWT `kid` header
	// and as the JWK `kid`. Derived from the public key so Azure AD's
	// JWKS cache keys on a value that changes when the key rotates.
	KeyID(ctx context.Context) (string, error)
}

Signer abstracts ECDSA (ES256) signing over a SHA-256 digest. Implementations delegate the actual private-key operation to a cloud KMS so CUDly never handles the private key material.

func NewSignerFromEnv

func NewSignerFromEnv(ctx context.Context) (Signer, error)

NewSignerFromEnv returns the Signer appropriate for the current deployment. The selection is driven by CUDLY_SOURCE_CLOUD with backend-specific env vars supplying the key identifier.

Returns nil Signer (and no error) when no signing key env var is set at all — callers can treat this as "OIDC issuer disabled", which is useful for local dev and for deployments that haven't yet opted into the federated flow.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL