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
- func ComputeKeyID(pub crypto.PublicKey) (string, error)
- func IssuerURL() string
- func Mint(ctx context.Context, signer Signer, claims map[string]any) (string, error)
- func PrimeIssuerURLFromLambda(ctx context.Context) error
- func SetIssuerURL(rawURL string) error
- type AWSKMSClient
- type AWSKMSSigner
- type AzureKeyVaultClient
- type AzureKeyVaultSigner
- type Discovery
- type GCPKMSClient
- type GCPKMSSigner
- type JWK
- type JWKS
- type LambdaFunctionURLClient
- type LocalSigner
- type Signer
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
PublicKey fetches the public half of the KMS key once and caches it. Subsequent calls return the cached value.
func (*AWSKMSSigner) Sign ¶
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 ¶
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 ¶
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 ¶
PublicKey fetches the public half of the KMS key once and caches it.
func (*GCPKMSSigner) Sign ¶
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.
type JWKS ¶
type JWKS struct {
Keys []JWK `json:"keys"`
}
JWKS is the container returned by /.well-known/jwks.json.
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.
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 ¶
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.