authenticate: add jwks and .well-known endpoint (#745)

Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
This commit is contained in:
Bobby DeSimone 2020-05-21 11:46:29 -07:00 committed by GitHub
parent 9b82954012
commit 3f1faf2e9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 367 additions and 78 deletions

View file

@ -10,6 +10,8 @@ import (
"html/template" "html/template"
"net/url" "net/url"
"gopkg.in/square/go-jose.v2"
"github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/cryptutil" "github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding"
@ -95,6 +97,8 @@ type Authenticate struct {
// cacheClient is the interface for setting and getting sessions from a cache // cacheClient is the interface for setting and getting sessions from a cache
cacheClient cache.Cacher cacheClient cache.Cacher
jwk *jose.JSONWebKeySet
templates *template.Template templates *template.Template
} }
@ -166,7 +170,7 @@ func New(opts config.Options) (*Authenticate, error) {
return nil, err return nil, err
} }
return &Authenticate{ a := &Authenticate{
RedirectURL: redirectURL, RedirectURL: redirectURL,
// shared state // shared state
sharedKey: opts.SharedKey, sharedKey: opts.SharedKey,
@ -183,7 +187,21 @@ func New(opts config.Options) (*Authenticate, error) {
provider: provider, provider: provider,
// grpc client for cache // grpc client for cache
cacheClient: cacheClient, cacheClient: cacheClient,
jwk: &jose.JSONWebKeySet{},
templates: template.Must(frontend.NewTemplates()),
}
templates: template.Must(frontend.NewTemplates()), if opts.SigningKey != "" {
}, nil decodedCert, err := base64.StdEncoding.DecodeString(opts.SigningKey)
if err != nil {
return nil, fmt.Errorf("authenticate: failed to decode signing key: %w", err)
}
jwk, err := cryptutil.PublicJWKFromBytes(decodedCert, jose.ES256)
if err != nil {
return nil, fmt.Errorf("authenticate: failed to convert jwks: %w", err)
}
a.jwk.Keys = append(a.jwk.Keys, *jwk)
}
return a, nil
} }

View file

@ -16,6 +16,8 @@ func newTestOptions(t *testing.T) *config.Options {
opts.Provider = "google" opts.Provider = "google"
opts.ClientSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=" opts.ClientSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw="
opts.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=" opts.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw="
opts.SigningKey = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJlMFRxbXJkSXBZWE03c3pSRERWYndXOS83RWJHVWhTdFFJalhsVHNXM1BvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFb0xaRDI2bEdYREhRQmhhZkdlbEVmRDdlNmYzaURjWVJPVjdUbFlIdHF1Y1BFL2hId2dmYQpNY3FBUEZsRmpueUpySXJhYTFlQ2xZRTJ6UktTQk5kNXBRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
err := opts.Validate() err := opts.Validate()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View file

@ -36,6 +36,7 @@ func (a *Authenticate) Handler() http.Handler {
// Mount mounts the authenticate routes to the given router. // Mount mounts the authenticate routes to the given router.
func (a *Authenticate) Mount(r *mux.Router) { func (a *Authenticate) Mount(r *mux.Router) {
r.StrictSlash(true)
r.Use(middleware.SetHeaders(httputil.HeadersContentSecurityPolicy)) r.Use(middleware.SetHeaders(httputil.HeadersContentSecurityPolicy))
r.Use(csrf.Protect( r.Use(csrf.Protect(
a.cookieSecret, a.cookieSecret,
@ -72,12 +73,55 @@ func (a *Authenticate) Mount(r *mux.Router) {
v.Path("/sign_out").Handler(httputil.HandlerFunc(a.SignOut)) v.Path("/sign_out").Handler(httputil.HandlerFunc(a.SignOut))
v.Path("/refresh").Handler(httputil.HandlerFunc(a.Refresh)).Methods(http.MethodGet) v.Path("/refresh").Handler(httputil.HandlerFunc(a.Refresh)).Methods(http.MethodGet)
wk := r.PathPrefix("/.well-known/pomerium").Subrouter()
wk.Path("/jwks.json").Handler(httputil.HandlerFunc(a.jwks)).Methods(http.MethodGet)
wk.Path("/").Handler(httputil.HandlerFunc(a.wellKnown)).Methods(http.MethodGet)
// https://www.googleapis.com/oauth2/v3/certs
// programmatic access api endpoint // programmatic access api endpoint
api := r.PathPrefix("/api").Subrouter() api := r.PathPrefix("/api").Subrouter()
api.Use(sessions.RetrieveSession(a.sessionLoaders...)) api.Use(sessions.RetrieveSession(a.sessionLoaders...))
api.Path("/v1/refresh").Handler(httputil.HandlerFunc(a.RefreshAPI)) api.Path("/v1/refresh").Handler(httputil.HandlerFunc(a.RefreshAPI))
} }
// Well-Known Uniform Resource Identifiers (URIs)
// https://en.wikipedia.org/wiki/List_of_/.well-known/_services_offered_by_webservers
func (a *Authenticate) wellKnown(w http.ResponseWriter, r *http.Request) error {
wellKnownURLS := struct {
// URL string referencing the client's JSON Web Key (JWK) Set
// RFC7517 document, which contains the client's public keys.
JSONWebKeySetURL string `json:"jwks_uri"`
OAuth2Callback string `json:"authentication_callback_endpoint"`
ProgrammaticRefreshAPI string `json:"api_refresh_endpoint"`
}{
a.RedirectURL.ResolveReference(&url.URL{Path: "/.well-known/pomerium/jwks.json"}).String(),
a.RedirectURL.ResolveReference(&url.URL{Path: "/oauth2/callback"}).String(),
a.RedirectURL.ResolveReference(&url.URL{Path: "/api/v1/refresh"}).String(),
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Content-Type-Options", "nosniff")
jBytes, err := json.Marshal(wellKnownURLS)
if err != nil {
return err
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "%s", jBytes)
return nil
}
func (a *Authenticate) jwks(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Content-Type-Options", "nosniff")
jBytes, err := json.Marshal(a.jwk)
if err != nil {
return err
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "%s", jBytes)
return nil
}
// VerifySession is the middleware used to enforce a valid authentication // VerifySession is the middleware used to enforce a valid authentication
// session state is attached to the users's request context. // session state is attached to the users's request context.
func (a *Authenticate) VerifySession(next http.Handler) http.Handler { func (a *Authenticate) VerifySession(next http.Handler) http.Handler {

View file

@ -167,6 +167,7 @@ Autocert requires that ports `80`/`443` be accessible from the internet in order
- Type: `string` pointing to the path of the directory - Type: `string` pointing to the path of the directory
- Required if using [Autocert](./#autocert) setting - Required if using [Autocert](./#autocert) setting
- Default: - Default:
- `/data/autocert` in published Pomerium docker images - `/data/autocert` in published Pomerium docker images
- [$XDG_DATA_HOME](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) - [$XDG_DATA_HOME](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
- `$HOME/.local/share/pomerium` - `$HOME/.local/share/pomerium`
@ -384,62 +385,62 @@ Expose a prometheus format HTTP endpoint on the specified port. Disabled by defa
**Metrics tracked** **Metrics tracked**
| Name | Type | Description | Name | Type | Description
| --------------------------------------------- | --------- | ----------------------------------------------------------------------- | --------------------------------------------- | --------- | -----------------------------------------------------------------------
| boltdb_free_alloc_size_bytes | Gauge | Bytes allocated in free pages | boltdb_free_alloc_size_bytes | Gauge | Bytes allocated in free pages
| boltdb_free_page_n | Gauge | Number of free pages on the freelist | boltdb_free_page_n | Gauge | Number of free pages on the freelist
| boltdb_freelist_inuse_size_bytes | Gauge | Bytes used by the freelist | boltdb_freelist_inuse_size_bytes | Gauge | Bytes used by the freelist
| boltdb_open_txn | Gauge | number of currently open read transactions | boltdb_open_txn | Gauge | number of currently open read transactions
| boltdb_pending_page_n | Gauge | Number of pending pages on the freelist | boltdb_pending_page_n | Gauge | Number of pending pages on the freelist
| boltdb_txn | Gauge | total number of started read transactions | boltdb_txn | Gauge | total number of started read transactions
| boltdb_txn_cursor_total | Counter | Total number of cursors created | boltdb_txn_cursor_total | Counter | Total number of cursors created
| boltdb_txn_node_deref_total | Counter | Total number of node dereferences | boltdb_txn_node_deref_total | Counter | Total number of node dereferences
| boltdb_txn_node_total | Counter | Total number of node allocations | boltdb_txn_node_total | Counter | Total number of node allocations
| boltdb_txn_page_alloc_size_bytes_total | Counter | Total bytes allocated | boltdb_txn_page_alloc_size_bytes_total | Counter | Total bytes allocated
| boltdb_txn_page_total | Counter | Total number of page allocations | boltdb_txn_page_total | Counter | Total number of page allocations
| boltdb_txn_rebalance_duration_ms_total | Counter | Total time spent rebalancing | boltdb_txn_rebalance_duration_ms_total | Counter | Total time spent rebalancing
| boltdb_txn_rebalance_total | Counter | Total number of node rebalances | boltdb_txn_rebalance_total | Counter | Total number of node rebalances
| boltdb_txn_spill_duration_ms_total | Counter | Total time spent spilling | boltdb_txn_spill_duration_ms_total | Counter | Total time spent spilling
| boltdb_txn_spill_total | Counter | Total number of nodes spilled | boltdb_txn_spill_total | Counter | Total number of nodes spilled
| boltdb_txn_split_total | Counter | Total number of nodes split | boltdb_txn_split_total | Counter | Total number of nodes split
| boltdb_txn_write_duration_ms_total | Counter | Total time spent writing to disk | boltdb_txn_write_duration_ms_total | Counter | Total time spent writing to disk
| boltdb_txn_write_total | Counter | Total number of writes performed | boltdb_txn_write_total | Counter | Total number of writes performed
| groupcache_cache_hits_total | Counter | Total cache hits in local or cluster cache | groupcache_cache_hits_total | Counter | Total cache hits in local or cluster cache
| groupcache_cache_hits_total | Counter | Total cache hits in local or cluster cache | groupcache_cache_hits_total | Counter | Total cache hits in local or cluster cache
| groupcache_gets_total | Counter | Total get request, including from peers | groupcache_gets_total | Counter | Total get request, including from peers
| groupcache_loads_deduped_total | Counter | gets without cache hits after duplicate suppression | groupcache_loads_deduped_total | Counter | gets without cache hits after duplicate suppression
| groupcache_loads_total | Counter | Total gets without cache hits | groupcache_loads_total | Counter | Total gets without cache hits
| groupcache_local_load_errs_total | Counter | Total local load errors | groupcache_local_load_errs_total | Counter | Total local load errors
| groupcache_local_loads_total | Counter | Total good local loads | groupcache_local_loads_total | Counter | Total good local loads
| groupcache_peer_errors_total | Counter | Total errors from peers | groupcache_peer_errors_total | Counter | Total errors from peers
| groupcache_peer_loads_total | Counter | Total remote loads or cache hits without error | groupcache_peer_loads_total | Counter | Total remote loads or cache hits without error
| groupcache_server_requests_total | Counter | Total gets from peers | groupcache_server_requests_total | Counter | Total gets from peers
| grpc_client_request_duration_ms | Histogram | GRPC client request duration by service | grpc_client_request_duration_ms | Histogram | GRPC client request duration by service
| grpc_client_request_size_bytes | Histogram | GRPC client request size by service | grpc_client_request_size_bytes | Histogram | GRPC client request size by service
| grpc_client_requests_total | Counter | Total GRPC client requests made by service | grpc_client_requests_total | Counter | Total GRPC client requests made by service
| grpc_client_response_size_bytes | Histogram | GRPC client response size by service | grpc_client_response_size_bytes | Histogram | GRPC client response size by service
| grpc_server_request_duration_ms | Histogram | GRPC server request duration by service | grpc_server_request_duration_ms | Histogram | GRPC server request duration by service
| grpc_server_request_size_bytes | Histogram | GRPC server request size by service | grpc_server_request_size_bytes | Histogram | GRPC server request size by service
| grpc_server_requests_total | Counter | Total GRPC server requests made by service | grpc_server_requests_total | Counter | Total GRPC server requests made by service
| grpc_server_response_size_bytes | Histogram | GRPC server response size by service | grpc_server_response_size_bytes | Histogram | GRPC server response size by service
| http_client_request_duration_ms | Histogram | HTTP client request duration by service | http_client_request_duration_ms | Histogram | HTTP client request duration by service
| http_client_request_size_bytes | Histogram | HTTP client request size by service | http_client_request_size_bytes | Histogram | HTTP client request size by service
| http_client_requests_total | Counter | Total HTTP client requests made by service | http_client_requests_total | Counter | Total HTTP client requests made by service
| http_client_response_size_bytes | Histogram | HTTP client response size by service | http_client_response_size_bytes | Histogram | HTTP client response size by service
| http_server_request_duration_ms | Histogram | HTTP server request duration by service | http_server_request_duration_ms | Histogram | HTTP server request duration by service
| http_server_request_size_bytes | Histogram | HTTP server request size by service | http_server_request_size_bytes | Histogram | HTTP server request size by service
| http_server_requests_total | Counter | Total HTTP server requests handled by service | http_server_requests_total | Counter | Total HTTP server requests handled by service
| http_server_response_size_bytes | Histogram | HTTP server response size by service | http_server_response_size_bytes | Histogram | HTTP server response size by service
| pomerium_build_info | Gauge | Pomerium build metadata by git revision, service, version and goversion | pomerium_build_info | Gauge | Pomerium build metadata by git revision, service, version and goversion
| pomerium_config_checksum_int64 | Gauge | Currently loaded configuration checksum by service | pomerium_config_checksum_int64 | Gauge | Currently loaded configuration checksum by service
| pomerium_config_last_reload_success | Gauge | Whether the last configuration reload succeeded by service | pomerium_config_last_reload_success | Gauge | Whether the last configuration reload succeeded by service
| pomerium_config_last_reload_success_timestamp | Gauge | The timestamp of the last successful configuration reload by service | pomerium_config_last_reload_success_timestamp | Gauge | The timestamp of the last successful configuration reload by service
| redis_conns | Gauge | Number of total connections in the pool | redis_conns | Gauge | Number of total connections in the pool
| redis_hits_total | Counter | Total number of times free connection was found in the pool | redis_hits_total | Counter | Total number of times free connection was found in the pool
| redis_idle_conns | Gauge | Number of idle connections in the pool | redis_idle_conns | Gauge | Number of idle connections in the pool
| redis_misses_total | Counter | Total number of times free connection was NOT found in the pool | redis_misses_total | Counter | Total number of times free connection was NOT found in the pool
| redis_stale_conns_total | Counter | Total number of stale connections removed from the pool | redis_stale_conns_total | Counter | Total number of stale connections removed from the pool
| redis_timeouts_total | Counter | Total number of times a wait timeout occurred | redis_timeouts_total | Counter | Total number of times a wait timeout occurred
### Tracing ### Tracing
@ -449,15 +450,14 @@ Each unit work is called a Span in a trace. Spans include metadata about the wor
#### Shared Tracing Settings #### Shared Tracing Settings
| Config Key | Description | Required | Config Key | Description | Required
| :------------------ | :------------------------------------------------------------------------------------ | -------- | :------------------ | :----------------------------------------------------------------------------------- | --------
| tracing_provider | The name of the tracing provider. (e.g. jaeger, zipkin) | ✅ | tracing_provider | The name of the tracing provider. (e.g. jaeger, zipkin) | ✅
| tracing_sample_rate | Percentage of requests to sample in decimal notation. Default is `0.0001`, or `.01%` | ❌ | tracing_sample_rate | Percentage of requests to sample in decimal notation. Default is `0.0001`, or `.01%` | ❌
#### Jaeger (partial) #### Jaeger (partial)
**Warning** At this time, Jaeger protocol does not capture spans inside the proxy service. Please **Warning** At this time, Jaeger protocol does not capture spans inside the proxy service. Please use Zipkin protocol with Jaeger for full support.
use Zipkin protocol with Jaeger for full support.
[Jaeger](https://www.jaegertracing.io/) is a distributed tracing system released as open source by Uber Technologies. It is used for monitoring and troubleshooting microservices-based distributed systems, including: [Jaeger](https://www.jaegertracing.io/) is a distributed tracing system released as open source by Uber Technologies. It is used for monitoring and troubleshooting microservices-based distributed systems, including:
@ -467,21 +467,20 @@ use Zipkin protocol with Jaeger for full support.
- Service dependency analysis - Service dependency analysis
- Performance / latency optimization - Performance / latency optimization
| Config Key | Description | Required | Config Key | Description | Required
| :-------------------------------- | :------------------------------------------ | -------- | :-------------------------------- | :------------------------------------------ | --------
| tracing_jaeger_collector_endpoint | Url to the Jaeger HTTP Thrift collector. | ✅ | tracing_jaeger_collector_endpoint | Url to the Jaeger HTTP Thrift collector. | ✅
| tracing_jaeger_agent_endpoint | Send spans to jaeger-agent at this address. | ✅ | tracing_jaeger_agent_endpoint | Send spans to jaeger-agent at this address. | ✅
#### Zipkin #### Zipkin
Zipkin is an open source distributed tracing system and protocol. Zipkin is an open source distributed tracing system and protocol.
Many tracing backends support zipkin either directly or through intermediary agents, including Jaeger. For full tracing support, we recommend using the Zipkin tracing protocol. Many tracing backends support zipkin either directly or through intermediary agents, including Jaeger. For full tracing support, we recommend using the Zipkin tracing protocol.
| Config Key | Description | Required |
| :---------------------- | :------------------------------- | -------- |
| tracing_zipkin_endpoint | Url to the Zipkin HTTP endpoint. | ✅ |
Config Key | Description | Required
:---------------------- | :------------------------------- | --------
tracing_zipkin_endpoint | Url to the Zipkin HTTP endpoint. | ✅
#### Example #### Example
@ -1034,9 +1033,31 @@ See [ProxyPreserveHost](http://httpd.apache.org/docs/2.0/mod/mod_proxy.html#prox
- Type: [base64 encoded] `string` - Type: [base64 encoded] `string`
- Optional - Optional
Signing key is the base64 encoded key used to sign outbound requests. For more information see the [signed headers] docs. Signing Key is the key used to sign a user's attestation JWT which can be consumed by upstream applications to pass along identifying user information like username, id, and groups.
If no certificate is specified, one will be generated for you and the base64'd public key will be added to the logs. If set, the signing key's public key will can retrieved by hitting Pomerium's `/.well-known/pomerium/jwks.json` endpoint which lives on the authenticate service. For example:
```bash
$ curl https://authenticate.int.example.com/.well-known/pomerium/jwks.json | jq
```
```json
{
"keys": [
{
"use": "sig",
"kty": "EC",
"kid": "ccc5bc9d835ff3c8f7075ed4a7510159cf440fd7bf7b517b5caeb1fa419ee6a1",
"crv": "P-256",
"alg": "ES256",
"x": "QCN7adG2AmIK3UdHJvVJkldsUc6XeBRz83Z4rXX8Va4",
"y": "PI95b-ary66nrvA55TpaiWADq8b3O1CYIbvjqIHpXCY"
}
]
}
```
If no certificate is specified, one will be generated and the base64'd public key will be added to the logs. Note, however, that this key be unique to each service, ephemeral, and will not be accessible via the authenticate service's `jwks_uri` endpoint.
[base64 encoded]: https://en.wikipedia.org/wiki/Base64 [base64 encoded]: https://en.wikipedia.org/wiki/Base64
[environmental variables]: https://en.wikipedia.org/wiki/Environment_variable [environmental variables]: https://en.wikipedia.org/wiki/Environment_variable

View file

@ -16,7 +16,7 @@ To secure your app with signed headers, you'll need the following:
## Verification ## Verification
A JWT attesting to the authorization of a given request is added to the downstream HTTP request header `x-pomerium-jwt-assertion`. You should verify that the JWT contains at least the following claims: If a [signing key] is set, the user's associated identity information will be included in a signed attestation JWT that will be added to each requests's upstream header `x-pomerium-jwt-assertion`. You should verify that the JWT contains at least the following claims:
[JWT] | description [JWT] | description
:------: | ------------------------------------------------------------------------------------------------------ :------: | ------------------------------------------------------------------------------------------------------
@ -28,11 +28,33 @@ A JWT attesting to the authorization of a given request is added to the downstre
`email` | Email is the user's email. Can be used instead of the `x-pomerium-authenticated-user-email` header. `email` | Email is the user's email. Can be used instead of the `x-pomerium-authenticated-user-email` header.
`groups` | Groups is the user's groups. Can be used instead of the `x-pomerium-authenticated-user-groups` header. `groups` | Groups is the user's groups. Can be used instead of the `x-pomerium-authenticated-user-groups` header.
The attestation JWT's signature can be verified using the public key which can be retrieved at Pomerium's `/.well-known/pomerium/jwks.json` endpoint which lives on the authenticate service. A `jwks_uri` is useful when integrating with other systems like [istio](https://istio.io/docs/reference/config/security/istio.authentication.v1alpha1/). For example:
```bash
$ curl https://authenticate.int.example.com/.well-known/pomerium/jwks.json | jq
```
```json
{
"keys": [
{
"use": "sig",
"kty": "EC",
"kid": "ccc5bc9d835ff3c8f7075ed4a7510159cf440fd7bf7b517b5caeb1fa419ee6a1",
"crv": "P-256",
"alg": "ES256",
"x": "QCN7adG2AmIK3UdHJvVJkldsUc6XeBRz83Z4rXX8Va4",
"y": "PI95b-ary66nrvA55TpaiWADq8b3O1CYIbvjqIHpXCY"
}
]
}
```
### Manual verification ### Manual verification
Though you will very likely be verifying signed-headers programmatically in your application's middleware, and using a third-party JWT library, if you are new to JWT it may be helpful to show what manual verification looks like. Though you will very likely be verifying signed-headers programmatically in your application's middleware, and using a third-party JWT library, if you are new to JWT it may be helpful to show what manual verification looks like.
1. Provide pomerium with a base64 encoded Elliptic Curve ([NIST P-256] aka [secp256r1] aka prime256v1) Private Key. In production, you'd likely want to get these from your KMS. 1\. Provide pomerium with a base64 encoded Elliptic Curve ([NIST P-256] aka [secp256r1] aka prime256v1) Private Key. In production, you'd likely want to get these from your KMS.
```bash ```bash
# see ./scripts/generate_self_signed_signing_key.sh # see ./scripts/generate_self_signed_signing_key.sh
@ -88,3 +110,4 @@ In the future, we will be adding example client implementations for:
[key management service]: https://en.wikipedia.org/wiki/Key_management [key management service]: https://en.wikipedia.org/wiki/Key_management
[nist p-256]: https://csrc.nist.gov/csrc/media/events/workshop-on-elliptic-curve-cryptography-standards/documents/papers/session6-adalier-mehmet.pdf [nist p-256]: https://csrc.nist.gov/csrc/media/events/workshop-on-elliptic-curve-cryptography-standards/documents/papers/session6-adalier-mehmet.pdf
[secp256r1]: https://wiki.openssl.org/index.php/Command_Line_Elliptic_Curve_Operations [secp256r1]: https://wiki.openssl.org/index.php/Command_Line_Elliptic_Curve_Operations
[signing key]: ./../../configuration/readme.md#signing-key

View file

@ -44,6 +44,8 @@ func (srv *Server) buildPomeriumHTTPRoutes(options *config.Options, domain strin
srv.buildControlPlanePathRoute("/healthz"), srv.buildControlPlanePathRoute("/healthz"),
srv.buildControlPlanePathRoute("/.pomerium"), srv.buildControlPlanePathRoute("/.pomerium"),
srv.buildControlPlanePrefixRoute("/.pomerium/"), srv.buildControlPlanePrefixRoute("/.pomerium/"),
srv.buildControlPlanePathRoute("/.well-known/pomerium"),
srv.buildControlPlanePrefixRoute("/.well-known/pomerium/"),
} }
// if we're handling authentication, add the oauth2 callback url // if we're handling authentication, add the oauth2 callback url
if config.IsAuthenticate(options.Services) && domain == options.AuthenticateURL.Host { if config.IsAuthenticate(options.Services) && domain == options.AuthenticateURL.Host {

View file

@ -0,0 +1,66 @@
package cryptutil
import (
"crypto"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"gopkg.in/square/go-jose.v2"
)
// PrivateJWKFromBytes returns a jose JSON Web _Private_ Key from bytes.
func PrivateJWKFromBytes(data []byte, alg jose.SignatureAlgorithm) (*jose.JSONWebKey, error) {
return loadKey(data, alg, func(b []byte) (interface{}, error) {
switch alg {
case jose.ES256, jose.ES384, jose.ES512:
return x509.ParseECPrivateKey(b)
case jose.RS256, jose.RS384, jose.RS512:
return x509.ParsePKCS1PrivateKey(b)
default:
return nil, errors.New("unsupported signature algorithm")
}
})
}
// PublicJWKFromBytes returns a jose JSON Web _Public_ Key from bytes.
func PublicJWKFromBytes(data []byte, alg jose.SignatureAlgorithm) (*jose.JSONWebKey, error) {
return loadKey(data, alg, func(b []byte) (interface{}, error) {
switch alg {
case jose.ES256, jose.ES384, jose.ES512:
key, err := x509.ParseECPrivateKey(b)
if err != nil {
return nil, err
}
return key.Public(), nil
case jose.RS256, jose.RS384, jose.RS512:
key, err := x509.ParsePKCS1PrivateKey(b)
if err != nil {
return nil, err
}
return key.Public(), nil
default:
return nil, errors.New("unsupported signature algorithm")
}
})
}
func loadKey(data []byte, alg jose.SignatureAlgorithm, unmarshal func([]byte) (interface{}, error)) (*jose.JSONWebKey, error) {
block, _ := pem.Decode(data)
if block == nil {
return nil, fmt.Errorf("file contained no PEM encoded data")
}
priv, err := unmarshal(block.Bytes)
if err != nil {
return nil, fmt.Errorf("unmarshal key: %w", err)
}
key := &jose.JSONWebKey{Key: priv, Use: "sig", Algorithm: string(alg)}
thumbprint, err := key.Thumbprint(crypto.SHA256)
if err != nil {
return nil, fmt.Errorf("computing thumbprint: %w", err)
}
key.KeyID = hex.EncodeToString(thumbprint)
return key, nil
}

View file

@ -0,0 +1,113 @@
package cryptutil
import (
"encoding/base64"
"encoding/json"
"testing"
"github.com/google/go-cmp/cmp"
"gopkg.in/square/go-jose.v2"
)
func TestPrivateJWKFromBytes(t *testing.T) {
t.Parallel()
tests := []struct {
name string
cert string
alg jose.SignatureAlgorithm
want string
wantErr bool
}{
{"good RS256",
"LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcGdJQkFBS0NBUUVBNjdLanFtUVlHcTBNVnRBQ1ZwZUNtWG1pbmxRYkRQR0xtc1pBVUV3dWVIUW5ydDNXCnR2cERPbTZBbGFKTVVuVytIdTU1ampva2FsS2VWalRLbWdZR2JxVXpWRG9NYlBEYUhla2x0ZEJUTUdsT1VGc1AKNFVKU0RyTzR6ZE4rem80MjhUWDJQbkcyRkNkVktHeTRQRThpbEhiV0xjcjg3MVlqVjUxZnc4Q0xEWDlQWkpOdQo4NjFDRjdWOWlFSm02c1NmUWxtbmhOOGozK1d6VmJQUU55MVdzUjdpOWU5ajYzRXFLdDIyUTlPWEwrV0FjS3NrCm9JU21DTlZSVUFqVThZUlZjZ1FKQit6UTM0QVFQbHowT3A1Ty9RTi9NZWRqYUY4d0xTK2l2L3p2aVM4Y3FQYngKbzZzTHE2Rk5UbHRrL1FreGVDZUtLVFFlLzNrUFl2UUFkbmw2NVFJREFRQUJBb0lCQVFEQVQ0eXN2V2pSY3pxcgpKcU9SeGFPQTJEY3dXazJML1JXOFhtQWhaRmRTWHV2MkNQbGxhTU1yelBmTG41WUlmaHQzSDNzODZnSEdZc3pnClo4aWJiYWtYNUdFQ0t5N3lRSDZuZ3hFS3pRVGpiampBNWR3S0h0UFhQUnJmamQ1Y2FMczVpcDcxaWxCWEYxU3IKWERIaXUycnFtaC9kVTArWGRMLzNmK2VnVDl6bFQ5YzRyUm84dnZueWNYejFyMnVhRVZ2VExsWHVsb2NpeEVrcgoySjlTMmxveWFUb2tFTnNlMDNpSVdaWnpNNElZcVowOGJOeG9IWCszQXVlWExIUStzRkRKMlhaVVdLSkZHMHUyClp3R2w3YlZpRTFQNXdiQUdtZzJDeDVCN1MrdGQyUEpSV3Frb2VxY3F2RVdCc3RFL1FEcDFpVThCOHpiQXd0Y3IKZHc5TXZ6Q2hBb0dCQVBObzRWMjF6MGp6MWdEb2tlTVN5d3JnL2E4RkJSM2R2Y0xZbWV5VXkybmd3eHVucnFsdwo2U2IrOWdrOGovcXEvc3VQSDhVdzNqSHNKYXdGSnNvTkVqNCt2b1ZSM3UrbE5sTEw5b21rMXBoU0dNdVp0b3huCm5nbUxVbkJUMGI1M3BURkJ5WGsveE5CbElreWdBNlg5T2MreW5na3RqNlRyVnMxUERTdnVJY0s1QW9HQkFQZmoKcEUzR2F6cVFSemx6TjRvTHZmQWJBdktCZ1lPaFNnemxsK0ZLZkhzYWJGNkdudFd1dWVhY1FIWFpYZTA1c2tLcApXN2xYQ3dqQU1iUXI3QmdlazcrOSszZElwL1RnYmZCYnN3Syt6Vng3Z2doeWMrdytXRWExaHByWTZ6YXdxdkFaCkhRU2lMUEd1UGp5WXBQa1E2ZFdEczNmWHJGZ1dlTmd4SkhTZkdaT05Bb0dCQUt5WTF3MUM2U3Y2c3VuTC8vNTcKQ2Z5NTAwaXlqNUZBOWRqZkRDNWt4K1JZMnlDV0ExVGsybjZyVmJ6dzg4czBTeDMrYS9IQW1CM2dMRXBSRU5NKwo5NHVwcENFWEQ3VHdlcGUxUnlrTStKbmp4TzlDSE41c2J2U25sUnBQWlMvZzJRTVhlZ3grK2trbkhXNG1ITkFyCndqMlRrMXBBczFXbkJ0TG9WaGVyY01jSkFvR0JBSTYwSGdJb0Y5SysvRUcyY21LbUg5SDV1dGlnZFU2eHEwK0IKWE0zMWMzUHE0amdJaDZlN3pvbFRxa2d0dWtTMjBraE45dC9ibkI2TmhnK1N1WGVwSXFWZldVUnlMejVwZE9ESgo2V1BMTTYzcDdCR3cwY3RPbU1NYi9VRm5Yd0U4OHlzRlNnOUF6VjdVVUQvU0lDYkI5ZHRVMWh4SHJJK0pZRWdWCkFrZWd6N2lCQW9HQkFJRncrQVFJZUIwM01UL0lCbGswNENQTDJEak0rNDhoVGRRdjgwMDBIQU9mUWJrMEVZUDEKQ2FLR3RDbTg2MXpBZjBzcS81REtZQ0l6OS9HUzNYRk00Qm1rRk9nY1NXVENPNmZmTGdLM3FmQzN4WDJudlpIOQpYZGNKTDQrZndhY0x4c2JJKzhhUWNOVHRtb3pkUjEzQnNmUmIrSGpUL2o3dkdrYlFnSkhCT0syegotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=",
jose.RS256,
`{"use":"sig","kty":"RSA","kid":"f0cc8033b422c2a199dcb456dde29589a9f5edd27d1c345bdf308957e957becf","alg":"RS256","n":"67KjqmQYGq0MVtACVpeCmXminlQbDPGLmsZAUEwueHQnrt3WtvpDOm6AlaJMUnW-Hu55jjokalKeVjTKmgYGbqUzVDoMbPDaHekltdBTMGlOUFsP4UJSDrO4zdN-zo428TX2PnG2FCdVKGy4PE8ilHbWLcr871YjV51fw8CLDX9PZJNu861CF7V9iEJm6sSfQlmnhN8j3-WzVbPQNy1WsR7i9e9j63EqKt22Q9OXL-WAcKskoISmCNVRUAjU8YRVcgQJB-zQ34AQPlz0Op5O_QN_MedjaF8wLS-iv_zviS8cqPbxo6sLq6FNTltk_QkxeCeKKTQe_3kPYvQAdnl65Q","e":"AQAB","d":"wE-MrL1o0XM6qyajkcWjgNg3MFpNi_0VvF5gIWRXUl7r9gj5ZWjDK8z3y5-WCH4bdx97POoBxmLM4GfIm22pF-RhAisu8kB-p4MRCs0E4244wOXcCh7T1z0a343eXGi7OYqe9YpQVxdUq1wx4rtq6pof3VNPl3S_93_noE_c5U_XOK0aPL758nF89a9rmhFb0y5V7paHIsRJK9ifUtpaMmk6JBDbHtN4iFmWczOCGKmdPGzcaB1_twLnlyx0PrBQydl2VFiiRRtLtmcBpe21YhNT-cGwBpoNgseQe0vrXdjyUVqpKHqnKrxFgbLRP0A6dYlPAfM2wMLXK3cPTL8woQ","p":"82jhXbXPSPPWAOiR4xLLCuD9rwUFHd29wtiZ7JTLaeDDG6euqXDpJv72CTyP-qr-y48fxTDeMewlrAUmyg0SPj6-hVHe76U2Usv2iaTWmFIYy5m2jGeeCYtScFPRvnelMUHJeT_E0GUiTKADpf05z7KeCS2PpOtWzU8NK-4hwrk","q":"9-OkTcZrOpBHOXM3igu98BsC8oGBg6FKDOWX4Up8expsXoae1a655pxAddld7TmyQqlbuVcLCMAxtCvsGB6Tv737d0in9OBt8FuzAr7NXHuCCHJz7D5YRrWGmtjrNrCq8BkdBKIs8a4-PJik-RDp1YOzd9esWBZ42DEkdJ8Zk40","dp":"rJjXDULpK_qy6cv__nsJ_LnTSLKPkUD12N8MLmTH5FjbIJYDVOTafqtVvPDzyzRLHf5r8cCYHeAsSlEQ0z73i6mkIRcPtPB6l7VHKQz4mePE70Ic3mxu9KeVGk9lL-DZAxd6DH76SScdbiYc0CvCPZOTWkCzVacG0uhWF6twxwk","dq":"jrQeAigX0r78QbZyYqYf0fm62KB1TrGrT4FczfVzc-riOAiHp7vOiVOqSC26RLbSSE3239ucHo2GD5K5d6kipV9ZRHIvPml04MnpY8szrensEbDRy06Ywxv9QWdfATzzKwVKD0DNXtRQP9IgJsH121TWHEesj4lgSBUCR6DPuIE","qi":"gXD4BAh4HTcxP8gGWTTgI8vYOMz7jyFN1C_zTTQcA59BuTQRg_UJooa0KbzrXMB_Syr_kMpgIjP38ZLdcUzgGaQU6BxJZMI7p98uArep8LfFfae9kf1d1wkvj5_BpwvGxsj7xpBw1O2ajN1HXcGx9Fv4eNP-Pu8aRtCAkcE4rbM"}`,
false,
},
{"good SS256",
"LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJlMFRxbXJkSXBZWE03c3pSRERWYndXOS83RWJHVWhTdFFJalhsVHNXM1BvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFb0xaRDI2bEdYREhRQmhhZkdlbEVmRDdlNmYzaURjWVJPVjdUbFlIdHF1Y1BFL2hId2dmYQpNY3FBUEZsRmpueUpySXJhYTFlQ2xZRTJ6UktTQk5kNXBRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=",
jose.ES256,
`{"use":"sig","kty":"EC","kid":"d591aa6e01e57ea8b80f349dc5de8517aa7b1f12f77700d89cbdba83938c0c61","crv":"P-256","alg":"ES256","x":"oLZD26lGXDHQBhafGelEfD7e6f3iDcYROV7TlYHtquc","y":"DxP4R8IH2jHKgDxZRY58iayK2mtXgpWBNs0SkgTXeaU","d":"F7ROqat0ilhczuzNEMNVvBb3_sRsZSFK1AiNeVOxbc8"}`,
false,
},
{"unknown signing key type",
"LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJlMFRxbXJkSXBZWE03c3pSRERWYndXOS83RWJHVWhTdFFJalhsVHNXM1BvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFb0xaRDI2bEdYREhRQmhhZkdlbEVmRDdlNmYzaURjWVJPVjdUbFlIdHF1Y1BFL2hId2dmYQpNY3FBUEZsRmpueUpySXJhYTFlQ2xZRTJ6UktTQk5kNXBRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=",
jose.HS256,
`null`,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := base64.StdEncoding.DecodeString(tt.cert)
if err != nil {
t.Fatal(err)
}
out, err := PrivateJWKFromBytes(data, tt.alg)
if (err != nil) != tt.wantErr {
t.Errorf("PrivateJWKFromBytes() error = %v, wantErr %v", err, tt.wantErr)
return
}
got, err := json.Marshal(out)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(got), tt.want); diff != "" {
t.Errorf("PrivateJWKFromBytes() want %v", diff)
}
})
}
}
func TestPublicJWKFromBytes(t *testing.T) {
t.Parallel()
tests := []struct {
name string
cert string
alg jose.SignatureAlgorithm
want string
wantErr bool
}{
{"good RS256",
"LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcGdJQkFBS0NBUUVBNjdLanFtUVlHcTBNVnRBQ1ZwZUNtWG1pbmxRYkRQR0xtc1pBVUV3dWVIUW5ydDNXCnR2cERPbTZBbGFKTVVuVytIdTU1ampva2FsS2VWalRLbWdZR2JxVXpWRG9NYlBEYUhla2x0ZEJUTUdsT1VGc1AKNFVKU0RyTzR6ZE4rem80MjhUWDJQbkcyRkNkVktHeTRQRThpbEhiV0xjcjg3MVlqVjUxZnc4Q0xEWDlQWkpOdQo4NjFDRjdWOWlFSm02c1NmUWxtbmhOOGozK1d6VmJQUU55MVdzUjdpOWU5ajYzRXFLdDIyUTlPWEwrV0FjS3NrCm9JU21DTlZSVUFqVThZUlZjZ1FKQit6UTM0QVFQbHowT3A1Ty9RTi9NZWRqYUY4d0xTK2l2L3p2aVM4Y3FQYngKbzZzTHE2Rk5UbHRrL1FreGVDZUtLVFFlLzNrUFl2UUFkbmw2NVFJREFRQUJBb0lCQVFEQVQ0eXN2V2pSY3pxcgpKcU9SeGFPQTJEY3dXazJML1JXOFhtQWhaRmRTWHV2MkNQbGxhTU1yelBmTG41WUlmaHQzSDNzODZnSEdZc3pnClo4aWJiYWtYNUdFQ0t5N3lRSDZuZ3hFS3pRVGpiampBNWR3S0h0UFhQUnJmamQ1Y2FMczVpcDcxaWxCWEYxU3IKWERIaXUycnFtaC9kVTArWGRMLzNmK2VnVDl6bFQ5YzRyUm84dnZueWNYejFyMnVhRVZ2VExsWHVsb2NpeEVrcgoySjlTMmxveWFUb2tFTnNlMDNpSVdaWnpNNElZcVowOGJOeG9IWCszQXVlWExIUStzRkRKMlhaVVdLSkZHMHUyClp3R2w3YlZpRTFQNXdiQUdtZzJDeDVCN1MrdGQyUEpSV3Frb2VxY3F2RVdCc3RFL1FEcDFpVThCOHpiQXd0Y3IKZHc5TXZ6Q2hBb0dCQVBObzRWMjF6MGp6MWdEb2tlTVN5d3JnL2E4RkJSM2R2Y0xZbWV5VXkybmd3eHVucnFsdwo2U2IrOWdrOGovcXEvc3VQSDhVdzNqSHNKYXdGSnNvTkVqNCt2b1ZSM3UrbE5sTEw5b21rMXBoU0dNdVp0b3huCm5nbUxVbkJUMGI1M3BURkJ5WGsveE5CbElreWdBNlg5T2MreW5na3RqNlRyVnMxUERTdnVJY0s1QW9HQkFQZmoKcEUzR2F6cVFSemx6TjRvTHZmQWJBdktCZ1lPaFNnemxsK0ZLZkhzYWJGNkdudFd1dWVhY1FIWFpYZTA1c2tLcApXN2xYQ3dqQU1iUXI3QmdlazcrOSszZElwL1RnYmZCYnN3Syt6Vng3Z2doeWMrdytXRWExaHByWTZ6YXdxdkFaCkhRU2lMUEd1UGp5WXBQa1E2ZFdEczNmWHJGZ1dlTmd4SkhTZkdaT05Bb0dCQUt5WTF3MUM2U3Y2c3VuTC8vNTcKQ2Z5NTAwaXlqNUZBOWRqZkRDNWt4K1JZMnlDV0ExVGsybjZyVmJ6dzg4czBTeDMrYS9IQW1CM2dMRXBSRU5NKwo5NHVwcENFWEQ3VHdlcGUxUnlrTStKbmp4TzlDSE41c2J2U25sUnBQWlMvZzJRTVhlZ3grK2trbkhXNG1ITkFyCndqMlRrMXBBczFXbkJ0TG9WaGVyY01jSkFvR0JBSTYwSGdJb0Y5SysvRUcyY21LbUg5SDV1dGlnZFU2eHEwK0IKWE0zMWMzUHE0amdJaDZlN3pvbFRxa2d0dWtTMjBraE45dC9ibkI2TmhnK1N1WGVwSXFWZldVUnlMejVwZE9ESgo2V1BMTTYzcDdCR3cwY3RPbU1NYi9VRm5Yd0U4OHlzRlNnOUF6VjdVVUQvU0lDYkI5ZHRVMWh4SHJJK0pZRWdWCkFrZWd6N2lCQW9HQkFJRncrQVFJZUIwM01UL0lCbGswNENQTDJEak0rNDhoVGRRdjgwMDBIQU9mUWJrMEVZUDEKQ2FLR3RDbTg2MXpBZjBzcS81REtZQ0l6OS9HUzNYRk00Qm1rRk9nY1NXVENPNmZmTGdLM3FmQzN4WDJudlpIOQpYZGNKTDQrZndhY0x4c2JJKzhhUWNOVHRtb3pkUjEzQnNmUmIrSGpUL2o3dkdrYlFnSkhCT0syegotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=",
jose.RS256,
`{"use":"sig","kty":"RSA","kid":"f0cc8033b422c2a199dcb456dde29589a9f5edd27d1c345bdf308957e957becf","alg":"RS256","n":"67KjqmQYGq0MVtACVpeCmXminlQbDPGLmsZAUEwueHQnrt3WtvpDOm6AlaJMUnW-Hu55jjokalKeVjTKmgYGbqUzVDoMbPDaHekltdBTMGlOUFsP4UJSDrO4zdN-zo428TX2PnG2FCdVKGy4PE8ilHbWLcr871YjV51fw8CLDX9PZJNu861CF7V9iEJm6sSfQlmnhN8j3-WzVbPQNy1WsR7i9e9j63EqKt22Q9OXL-WAcKskoISmCNVRUAjU8YRVcgQJB-zQ34AQPlz0Op5O_QN_MedjaF8wLS-iv_zviS8cqPbxo6sLq6FNTltk_QkxeCeKKTQe_3kPYvQAdnl65Q","e":"AQAB"}`,
false,
},
{"good ES256",
"LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJlMFRxbXJkSXBZWE03c3pSRERWYndXOS83RWJHVWhTdFFJalhsVHNXM1BvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFb0xaRDI2bEdYREhRQmhhZkdlbEVmRDdlNmYzaURjWVJPVjdUbFlIdHF1Y1BFL2hId2dmYQpNY3FBUEZsRmpueUpySXJhYTFlQ2xZRTJ6UktTQk5kNXBRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=",
jose.ES256,
`{"use":"sig","kty":"EC","kid":"d591aa6e01e57ea8b80f349dc5de8517aa7b1f12f77700d89cbdba83938c0c61","crv":"P-256","alg":"ES256","x":"oLZD26lGXDHQBhafGelEfD7e6f3iDcYROV7TlYHtquc","y":"DxP4R8IH2jHKgDxZRY58iayK2mtXgpWBNs0SkgTXeaU"}`,
false,
},
{"unknown signing key type",
"LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJlMFRxbXJkSXBZWE03c3pSRERWYndXOS83RWJHVWhTdFFJalhsVHNXM1BvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFb0xaRDI2bEdYREhRQmhhZkdlbEVmRDdlNmYzaURjWVJPVjdUbFlIdHF1Y1BFL2hId2dmYQpNY3FBUEZsRmpueUpySXJhYTFlQ2xZRTJ6UktTQk5kNXBRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=",
jose.HS256,
`null`,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := base64.StdEncoding.DecodeString(tt.cert)
if err != nil {
t.Fatal(err)
}
out, err := PublicJWKFromBytes(data, tt.alg)
if (err != nil) != tt.wantErr {
t.Errorf("PublicJWKFromBytes() error = %v, wantErr %v", err, tt.wantErr)
return
}
got, err := json.Marshal(out)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(got), tt.want); diff != "" {
t.Errorf("PublicJWKFromBytes() want %v", diff)
}
})
}
}