From 832f8afbb660d7168ade2adec2b4c59df990f5e4 Mon Sep 17 00:00:00 2001 From: Mart Aarma Date: Wed, 7 Jul 2021 15:27:11 +0300 Subject: [PATCH] Hardware Security Module support for keys hydra.openid.id-token, hydra.jwt.access-token --- .docker/Dockerfile-hsm | 56 +++++++++++++++ docs/docs/hsm-support.md | 91 ++++++++++++++++++++++++ docs/docs/index.md | 5 ++ docs/docs/reference/configuration.md | 23 ++++++ driver/config/provider.go | 26 +++++++ driver/registry_base.go | 61 +++++++++++++--- driver/registry_base_test.go | 1 + go.mod | 5 +- go.sum | 10 +++ internal/.hydra.yaml | 3 + internal/config/config.yaml | 9 +++ jwk/handler.go | 43 ++++++++---- jwk/jwt_strategy.go | 101 +++++++++++++++------------ jwk/jwt_strategy_test.go | 2 +- jwk/registry.go | 2 + quickstart-hsm.yml | 85 ++++++++++++++++++++++ spec/config.json | 25 +++++++ 17 files changed, 480 insertions(+), 68 deletions(-) create mode 100644 .docker/Dockerfile-hsm create mode 100644 docs/docs/hsm-support.md create mode 100644 quickstart-hsm.yml diff --git a/.docker/Dockerfile-hsm b/.docker/Dockerfile-hsm new file mode 100644 index 00000000000..ce96054524c --- /dev/null +++ b/.docker/Dockerfile-hsm @@ -0,0 +1,56 @@ +FROM golang:1.16-alpine AS builder + +RUN apk -U --no-cache add build-base git gcc bash + +WORKDIR /go/src/github.com/ory/hydra + +ADD go.mod go.mod +ADD go.sum go.sum + +ENV GO111MODULE on +ENV CGO_ENABLED 1 + +RUN go mod download + +ADD . . + +RUN go build -tags sqlite -o /usr/bin/hydra + +FROM alpine:3.13.4 + +RUN apk -U --no-cache add softhsm opensc + +RUN pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot 0 --init-token --so-pin 0000 --init-pin --pin 1234 --label hydra \ + && pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \ + --login --pin 1234 --token-label hydra \ + --keypairgen --key-type rsa:4096 --usage-sign \ + --label hydra.openid.id-token --id 68796472612e6f70656e69642e69642d746f6b656e \ + && pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \ + --login --pin 1234 --token-label hydra \ + --keypairgen --key-type rsa:4096 --usage-sign \ + --label hydra.jwt.access-token --id 68796472612e6a77742e6163636573732d746f6b656e + +RUN addgroup -S ory; \ + adduser -S ory -G ory -D -h /home/ory -s /bin/nologin; \ + chown -R ory:ory /home/ory; \ + chown -R ory:ory /var/lib/softhsm/tokens + +COPY --from=builder /usr/bin/hydra /usr/bin/hydra + +# By creating the sqlite folder as the ory user, the mounted volume will be owned by ory:ory, which +# is required for read/write of SQLite. +RUN mkdir -p /var/lib/sqlite +RUN chown ory:ory /var/lib/sqlite +VOLUME /var/lib/sqlite + +# Exposing the ory home directory to simplify passing in hydra configuration (e.g. if the file $HOME/.hydra.yaml +# exists, it will be automatically used as the configuration file). +VOLUME /home/ory + +# Declare the standard ports used by hydra (4433 for public service endpoint, 4434 for admin service endpoint) +EXPOSE 4444 4445 + +USER ory + +ENTRYPOINT ["hydra"] +CMD ["serve"] diff --git a/docs/docs/hsm-support.md b/docs/docs/hsm-support.md new file mode 100644 index 00000000000..1c38c3c824a --- /dev/null +++ b/docs/docs/hsm-support.md @@ -0,0 +1,91 @@ +--- +id: hsm +title: Hardware Security Module support for JSON Web Key Sets +--- + +The PKCS#11 Cryptographic Token Interface Standard, also known as Cryptoki, is one of the Public Key Cryptography Standards developed by RSA Security. PKCS#11 defines the interface between an application and a cryptographic device. + +PKCS#11 is used as a low-level interface to perform cryptographic operations without the need for the application to directly interface a device through its driver. PKCS#11 represents cryptographic devices using a common model referred to simply as a token. An application can therefore perform cryptographic operations on any device or token, using the same independent command set. + +### HSM configuration +``` +HSM_ENABLED=true +HSM_LIBRARY=/path/to/hsm-vendor/library.so +HSM_TOKEN_LABEL=hydra +HSM_PIN=1234 +``` + +It is expected that token with label `hydra` contains RSA key pairs with labels `hydra.openid.id-token` and additionally `hydra.jwt.access-token` depending on ORY Hydra configuration. + +When generating keys on HSM, key `id` is used as `kid` in JSON Web Key Set. + +### Testing with SoftHSM + +Change into the directory with the Hydra source code and run the following +command to start the needed containers with SoftHSM support: + +```shell +$ docker-compose -f quickstart-hsm.yml up --build +``` + +On start up, ORY Hydra should inform if HSM is configured. Let's take a look at the logs: + +```shell +$ docker logs ory-hydra-example--hydra +time="2021-07-07T12:51:23Z" level=info msg="Hardware Security Module is configured." +time="2021-07-07T12:51:23Z" level=info msg="Using key pair 'hydra.openid.id-token' from Hardware Security Module." +time="2021-07-07T12:51:23Z" level=info msg="Using key pair 'hydra.jwt.access-token' from Hardware Security Module." +``` + +### Generating key pairs + +Depending on HSM vendor, tools generating/importing keys vary. Let's take a look how key pairs are generated in HSM quickstart container using `pkcs11-tool` from OpenSC: + +Creating token +```shell +$ pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --slot 0 --init-token --so-pin 0000 --init-pin --pin 1234 --label hydra + +Using slot 0 with a present token (0x2763db07) +Token successfully initialized +User PIN successfully initialized +``` + +Where parameter `--label hydra` value corresponds to value used in configuration `HSM_TOKEN_LABEL` and `--pin 1234` to `HSM_PIN` + +Generating keypair for JSON Web Key `hydra.openid.id-token` +```shell +$ pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \ +--login --pin 1234 --token-label hydra \ +--keypairgen --key-type rsa:4096 --usage-sign \ +--label hydra.openid.id-token --id 68796472612e6f70656e69642e69642d746f6b656e + +Key pair generated: +Private Key Object; RSA + label: hydra.openid.id-token + ID: 68796472612e6f70656e69642e69642d746f6b656e + Usage: decrypt, sign, unwrap +Public Key Object; RSA 4096 bits + label: hydra.openid.id-token + ID: 68796472612e6f70656e69642e69642d746f6b656e + Usage: encrypt, verify, wrap +``` + +Where parameter `--id 68796472612e6f70656e69642e69642d746f6b656e` is the value used as `kid` in JSON Web Key Set. It must be set as a big-endian hexadecimal integer value. + +Generating keypair for JSON Web Key `hydra.openid.id-token` +```shell +$ pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \ + --login --pin 1234 --token-label hydra \ + --keypairgen --key-type rsa:4096 --usage-sign \ + --label hydra.jwt.access-token --id 68796472612e6a77742e6163636573732d746f6b656e + +Key pair generated: +Private Key Object; RSA + label: hydra.jwt.access-token + ID: 68796472612e6a77742e6163636573732d746f6b656e + Usage: decrypt, sign, unwrap +Public Key Object; RSA 4096 bits + label: hydra.jwt.access-token + ID: 68796472612e6a77742e6163636573732d746f6b656e + Usage: encrypt, verify, wrap +``` diff --git a/docs/docs/index.md b/docs/docs/index.md index 291111ff66c..3316a3710bb 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -38,6 +38,11 @@ In addition to the OAuth 2.0 functionality, ORY Hydra offers a safe storage for cryptographic keys (used for example to sign JSON Web Tokens) and can manage OAuth 2.0 Clients. +### Hardware Security Module support + +ORY Hydra also offers a safe storage for cryptographic keys using HSM. +[Learn more](./hsm-support.md). + ## Security First ORY Hydra's architecture and work flows are designed to neutralize many common diff --git a/docs/docs/reference/configuration.md b/docs/docs/reference/configuration.md index 16863b1f890..58794f019ab 100644 --- a/docs/docs/reference/configuration.md +++ b/docs/docs/reference/configuration.md @@ -713,6 +713,29 @@ serve: # dsn: '' +## hsm ## +# Configures Hardware Security Module for hydra.openid.id-token, hydra.jwt.access-token keys +# Either slot or token_label must be set. If token_label is set, then first slot in index with this label is used. +# +# Set this value using environment variables on +# - Linux/macOS: +# $ export HSM_ENABLED= +# $ export HSM_PIN= +# $ export HSM_SLOT= +# $ export HSM_TOKEN_LABEL= +# - Windows Command Line (CMD): +# > set HSM_ENABLED= +# > set HSM_PIN= +# > set HSM_SLOT= +# > set HSM_TOKEN_LABEL= +# +hsm: + enabled: false + library: /path/to/hsm-vendor/library.so + pin: partition-pin-code + slot: 0 + token_label: hydra + ## webfinger ## # # Configures ./well-known/ settings. diff --git a/driver/config/provider.go b/driver/config/provider.go index e8bff7ef09e..8041282d4e0 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -24,6 +24,11 @@ import ( const ( KeyRoot = "" + HsmEnabled = "hsm.enabled" + HsmLibraryPath = "hsm.library" + HsmPin = "hsm.pin" + HsmSlotNumber = "hsm.slot" + HsmTokenLabel = "hsm.token_label" KeyWellKnownKeys = "webfinger.jwks.broadcast_keys" KeyOAuth2ClientRegistrationURL = "webfinger.oidc_discovery.client_registration_url" KeyOAuth2TokenURL = "webfinger.oidc_discovery.token_url" // #nosec G101 @@ -428,3 +433,24 @@ func (p *Provider) CGroupsV1AutoMaxProcsEnabled() bool { func (p *Provider) GrantAllClientCredentialsScopesPerDefault() bool { return p.p.Bool(KeyGrantAllClientCredentialsScopesPerDefault) } + +func (p *Provider) HsmEnabled() bool { + return p.p.Bool(HsmEnabled) +} + +func (p *Provider) HsmLibraryPath() string { + return p.p.String(HsmLibraryPath) +} + +func (p *Provider) HsmSlotNumber() *int { + n := p.p.Int(HsmSlotNumber) + return &n +} + +func (p *Provider) HsmPin() string { + return p.p.String(HsmPin) +} + +func (p *Provider) HsmTokenLabel() string { + return p.p.String(HsmTokenLabel) +} diff --git a/driver/registry_base.go b/driver/registry_base.go index e1013c76729..eb5947c60a6 100644 --- a/driver/registry_base.go +++ b/driver/registry_base.go @@ -2,7 +2,9 @@ package driver import ( "context" + "crypto/rsa" "fmt" + "github.com/ThalesIgnite/crypto11" "net" "net/http" "strings" @@ -58,6 +60,7 @@ type RegistryBase struct { fsc fosite.ScopeStrategy atjs jwk.JWTStrategy idtjs jwk.JWTStrategy + hsm *crypto11.Context fscPrev string fos *openid.DefaultStrategy forv *openid.OpenIDConnectRequestValidator @@ -344,18 +347,22 @@ func (m *RegistryBase) ScopeStrategy() fosite.ScopeStrategy { } func (m *RegistryBase) newKeyStrategy(key string) (s jwk.JWTStrategy) { - if err := jwk.EnsureAsymmetricKeypairExists(context.Background(), m.r, new(jwk.RS256Generator), key); err != nil { - var netError net.Error - if errors.As(err, &netError) { - m.Logger().WithError(err).Fatalf(`Could not ensure that signing keys for "%s" exists. A network error occurred, see error for specific details.`, key) - return - } + if m.C.HsmEnabled() { + m.EnsureHsmKeypairExists(key) + } else { + if err := jwk.EnsureAsymmetricKeypairExists(context.Background(), m.r, new(jwk.RS256Generator), key); err != nil { + var netError net.Error + if errors.As(err, &netError) { + m.Logger().WithError(err).Fatalf(`Could not ensure that signing keys for "%s" exists. A network error occurred, see error for specific details.`, key) + return + } - m.Logger().WithError(err).Fatalf(`Could not ensure that signing keys for "%s" exists. If you are running against a persistent SQL database this is most likely because your "secrets.system" ("SECRETS_SYSTEM" environment variable) is not set or changed. When running with an SQL database backend you need to make sure that the secret is set and stays the same, unless when doing key rotation. This may also happen when you forget to run "hydra migrate sql"..`, key) + m.Logger().WithError(err).Fatalf(`Could not ensure that signing keys for "%s" exists. If you are running against a persistent SQL database this is most likely because your "secrets.system" ("SECRETS_SYSTEM" environment variable) is not set or changed. When running with an SQL database backend you need to make sure that the secret is set and stays the same, unless when doing key rotation. This may also happen when you forget to run "hydra migrate sql"..`, key) + } } if err := resilience.Retry(m.Logger(), time.Second*15, time.Minute*15, func() (err error) { - s, err = jwk.NewRS256JWTStrategy(m.r, func() string { + s, err = jwk.NewRS256JWTStrategy(*m.C, m.r, func() string { return key }) return err @@ -367,7 +374,7 @@ func (m *RegistryBase) newKeyStrategy(key string) (s jwk.JWTStrategy) { } func (m *RegistryBase) AccessTokenJWTStrategy() jwk.JWTStrategy { - if m.atjs == nil { + if m.atjs == nil && m.C.IsUsingJWTAsAccessTokens() { m.atjs = m.newKeyStrategy(x.OAuth2JWTKeyName) } return m.atjs @@ -465,3 +472,39 @@ func (m *RegistryBase) WithOAuth2Provider(f fosite.OAuth2Provider) { func (m *RegistryBase) WithConsentStrategy(c consent.Strategy) { m.cos = c } + +func (m *RegistryBase) HardwareSecurityModule() *crypto11.Context { + if m.hsm == nil && m.C.HsmEnabled() { + config11 := &crypto11.Config{ + Path: m.C.HsmLibraryPath(), + Pin: m.C.HsmPin(), + } + + if m.C.HsmTokenLabel() != "" { + config11.TokenLabel = m.C.HsmTokenLabel() + } else { + config11.SlotNumber = m.C.HsmSlotNumber() + } + + ctx11, err := crypto11.Configure(config11) + if err != nil { + m.Logger().WithError(err).Fatalf("Unable to configure Hardware Security Module. Library path: %s, slot: %v, token label: %s", + m.C.HsmLibraryPath(), *m.C.HsmSlotNumber(), m.C.HsmTokenLabel()) + } else { + m.Logger().Info("Hardware Security Module is configured.") + } + + m.hsm = ctx11 + } + return m.hsm +} + +func (m *RegistryBase) EnsureHsmKeypairExists(keyAlias string) { + if keyPair, err := m.HardwareSecurityModule().FindKeyPair(nil, []byte(keyAlias)); err != nil { + m.Logger().WithError(err).Fatalf(`Could not ensure that signing keys for "%s" exists on Hardware Security Module. `, keyAlias) + } else if _, isRSA := keyPair.Public().(*rsa.PublicKey); !isRSA || keyPair == nil { + m.Logger().WithError(err).Fatalf(`Key pair "%s" on Hardware Security Module is not RSA key`, keyAlias) + } else { + m.Logger().Infof(`Using key pair "%s" from Hardware Security Module.`, keyAlias) + } +} diff --git a/driver/registry_base_test.go b/driver/registry_base_test.go index 3e1fb19b679..8582f036058 100644 --- a/driver/registry_base_test.go +++ b/driver/registry_base_test.go @@ -37,6 +37,7 @@ func TestRegistryBase_newKeyStrategy_handlesNetworkError(t *testing.T) { } registryBase := RegistryBase{r: registry, l: l} + registryBase.WithConfig(c) strategy := registryBase.newKeyStrategy("key") diff --git a/go.mod b/go.mod index e84c54ae83f..a5a09052895 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,10 @@ replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 replace github.com/oleiade/reflections => github.com/oleiade/reflections v1.0.1 +replace github.com/ory/fosite => github.com/aarmam/fosite v0.40.2-0.20210730112747-f73a3b4fda63 + require ( + github.com/ThalesIgnite/crypto11 v1.2.4 github.com/cenkalti/backoff/v3 v3.0.0 github.com/evanphx/json-patch v0.5.2 github.com/go-bindata/go-bindata v3.1.1+incompatible @@ -63,5 +66,5 @@ require ( golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 golang.org/x/tools v0.1.0 gopkg.in/DataDog/dd-trace-go.v1 v1.27.1 - gopkg.in/square/go-jose.v2 v2.5.1 + gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614 ) diff --git a/go.sum b/go.sum index e2ae256948d..2d779f3b45b 100644 --- a/go.sum +++ b/go.sum @@ -61,7 +61,11 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/ThalesIgnite/crypto11 v1.2.4 h1:3MebRK/U0mA2SmSthXAIZAdUA9w8+ZuKem2O6HuR1f8= +github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/aarmam/fosite v0.40.2-0.20210730112747-f73a3b4fda63 h1:kUaJOzFoarUcyH0xUr1CFJ1HGHrBQ0H4Dc8HXMWd+aU= +github.com/aarmam/fosite v0.40.2-0.20210730112747-f73a3b4fda63/go.mod h1:cYorx8NtewqHVcTvBiuyra/Cp8mJAGstwZ6KnNXpHnk= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f h1:zvClvFQwU++UpIUBGC8YmDlfhUrweEy1R1Fj1gu5iIM= @@ -997,6 +1001,8 @@ github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00v github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f h1:eVB9ELsoq5ouItQBr5Tj334bhPJG/MX+m7rTchmzVUQ= +github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -1338,6 +1344,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/subosito/gotenv v1.1.1/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= +github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= github.com/tidwall/gjson v1.7.1 h1:hwkZ6V1/EF8FxNhKJrIXQwSscyl2yWCZ1SkOCQYHSHA= @@ -1944,6 +1952,8 @@ gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76 gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614 h1:lwJmuuJQGclcankpPJwh8rorzB0bNbVALv8phDGh8TQ= +gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= diff --git a/internal/.hydra.yaml b/internal/.hydra.yaml index 219229f2b2a..cb8f01245dc 100644 --- a/internal/.hydra.yaml +++ b/internal/.hydra.yaml @@ -64,6 +64,9 @@ serve: dsn: memory +hsm: + enabled: false + webfinger: jwks: broadcast_keys: diff --git a/internal/config/config.yaml b/internal/config/config.yaml index 22a22812513..32120df093b 100644 --- a/internal/config/config.yaml +++ b/internal/config/config.yaml @@ -267,6 +267,15 @@ dsn: memory # dsn: postgres://user:password@host:123/database # dsn: mysql://user:password@tcp(host:123)/database +# hsm configures Hardware Security Module for hydra.openid.id-token, hydra.jwt.access-token keys +# Either slot or token_label must be set. If token_label is set, then first slot in index with this label is used. +hsm: + enabled: false + library: /path/to/hsm-vendor/library.so + pin: partition-pin-code + slot: 0 + token_label: hydra + # webfinger configures ./well-known/ settings webfinger: # jwks configures the /.well-known/jwks.json endpoint. diff --git a/jwk/handler.go b/jwk/handler.go index 0eeac5f5d44..e69790cdde1 100644 --- a/jwk/handler.go +++ b/jwk/handler.go @@ -23,6 +23,7 @@ package jwk import ( "encoding/json" "fmt" + "github.com/ThalesIgnite/crypto11" "net/http" "github.com/ory/hydra/driver/config" @@ -91,21 +92,37 @@ func (h *Handler) WellKnown(w http.ResponseWriter, r *http.Request) { var jwks jose.JSONWebKeySet for _, set := range stringslice.Unique(h.c.WellKnownKeys()) { - keys, err := h.r.KeyManager().GetKeySet(r.Context(), set) - if err != nil { - h.r.Writer().WriteError(w, r, err) - return + if h.c.HsmEnabled() { + keyPair, err := h.r.HardwareSecurityModule().FindKeyPair(nil, []byte(set)) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + keyId, _ := h.r.HardwareSecurityModule().GetAttribute(keyPair, crypto11.CkaId) + keys := &jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{ + Algorithm: "RS256", + Use: "sig", + Key: keyPair.Public(), + KeyID: fmt.Sprintf("public:%s", keyId.Value), + }}} + + jwks.Keys = append(jwks.Keys, keys.Keys...) + } else { + keys, err := h.r.KeyManager().GetKeySet(r.Context(), set) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + keys, err = FindKeysByPrefix(keys, "public") + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + jwks.Keys = append(jwks.Keys, keys.Keys...) } - - keys, err = FindKeysByPrefix(keys, "public") - if err != nil { - h.r.Writer().WriteError(w, r, err) - return - } - - jwks.Keys = append(jwks.Keys, keys.Keys...) } - h.r.Writer().Write(w, r, &jwks) } diff --git a/jwk/jwt_strategy.go b/jwk/jwt_strategy.go index 3b72d06113e..18a3a621178 100644 --- a/jwk/jwt_strategy.go +++ b/jwk/jwt_strategy.go @@ -23,6 +23,7 @@ package jwk import ( "context" "crypto/rsa" + "github.com/ThalesIgnite/crypto11" "strings" "sync" @@ -33,6 +34,7 @@ import ( jwt2 "github.com/ory/fosite/token/jwt" "github.com/ory/fosite/token/jwt" + "gopkg.in/square/go-jose.v2/cryptosigner" ) type JWTStrategy interface { @@ -55,8 +57,8 @@ type RS256JWTStrategy struct { privateKeyID string } -func NewRS256JWTStrategy(r InternalRegistry, rs func() string) (*RS256JWTStrategy, error) { - j := &RS256JWTStrategy{r: r, rs: rs, RS256JWTStrategy: new(jwt.RS256JWTStrategy)} +func NewRS256JWTStrategy(c config.Provider, r InternalRegistry, rs func() string) (*RS256JWTStrategy, error) { + j := &RS256JWTStrategy{c: &c, r: r, rs: rs, RS256JWTStrategy: new(jwt.RS256JWTStrategy)} if err := j.refresh(context.TODO()); err != nil { return nil, err } @@ -109,49 +111,60 @@ func (j *RS256JWTStrategy) GetPublicKeyID(ctx context.Context) (string, error) { } func (j *RS256JWTStrategy) refresh(ctx context.Context) error { - keys, err := j.r.KeyManager().GetKeySet(ctx, j.rs()) - if err != nil { - return err - } - - public, err := FindKeyByPrefix(keys, "public") - if err != nil { - return err - } - - private, err := FindKeyByPrefix(keys, "private") - if err != nil { - return err - } - - if strings.Replace(public.KeyID, "public:", "", 1) != strings.Replace(private.KeyID, "private:", "", 1) { - return errors.New("public and private key pair kids do not match") - } - - if k, ok := private.Key.(*rsa.PrivateKey); !ok { - return errors.New("unable to type assert key to *rsa.PublicKey") - } else { - j.Lock() - j.privateKey = k - j.RS256JWTStrategy.PrivateKey = k - j.Unlock() - } - - if k, ok := public.Key.(*rsa.PublicKey); !ok { - return errors.New("unable to type assert key to *rsa.PublicKey") + if j.c.HsmEnabled() { + if keyPair, err := j.r.HardwareSecurityModule().FindKeyPair(nil, []byte(j.rs())); err != nil { + return err + } else { + j.Lock() + keyId, _ := j.r.HardwareSecurityModule().GetAttribute(keyPair, crypto11.CkaId) + j.RS256JWTStrategy.PrivateKey = cryptosigner.Opaque(keyPair) + j.publicKeyID = string(keyId.Value) + j.Unlock() + } } else { - j.Lock() - j.publicKey = k - j.publicKeyID = public.KeyID - j.Unlock() + keys, err := j.r.KeyManager().GetKeySet(ctx, j.rs()) + if err != nil { + return err + } + + public, err := FindKeyByPrefix(keys, "public") + if err != nil { + return err + } + + private, err := FindKeyByPrefix(keys, "private") + if err != nil { + return err + } + + if strings.Replace(public.KeyID, "public:", "", 1) != strings.Replace(private.KeyID, "private:", "", 1) { + return errors.New("public and private key pair kids do not match") + } + + if k, ok := private.Key.(*rsa.PrivateKey); !ok { + return errors.New("unable to type assert key to *rsa.PublicKey") + } else { + j.Lock() + j.privateKey = k + j.RS256JWTStrategy.PrivateKey = k + j.Unlock() + } + + if k, ok := public.Key.(*rsa.PublicKey); !ok { + return errors.New("unable to type assert key to *rsa.PublicKey") + } else { + j.Lock() + j.publicKey = k + j.publicKeyID = public.KeyID + j.Unlock() + } + + j.RLock() + defer j.RUnlock() + if j.privateKey.PublicKey.E != j.publicKey.E || + j.privateKey.PublicKey.N.String() != j.publicKey.N.String() { + return errors.New("public and private key pair fetched from store does not match") + } } - - j.RLock() - defer j.RUnlock() - if j.privateKey.PublicKey.E != j.publicKey.E || - j.privateKey.PublicKey.N.String() != j.publicKey.N.String() { - return errors.New("public and private key pair fetched from store does not match") - } - return nil } diff --git a/jwk/jwt_strategy_test.go b/jwk/jwt_strategy_test.go index d6c0161b1cc..12e00d86d34 100644 --- a/jwk/jwt_strategy_test.go +++ b/jwk/jwt_strategy_test.go @@ -46,7 +46,7 @@ func TestRS256JWTStrategy(t *testing.T) { require.NoError(t, err) require.NoError(t, m.AddKeySet(context.TODO(), "foo-set", ks)) - s, err := NewRS256JWTStrategy(reg, func() string { + s, err := NewRS256JWTStrategy(*conf, reg, func() string { return "foo-set" }) diff --git a/jwk/registry.go b/jwk/registry.go index d659d561356..3a461ad02ef 100644 --- a/jwk/registry.go +++ b/jwk/registry.go @@ -1,6 +1,7 @@ package jwk import ( + "github.com/ThalesIgnite/crypto11" "github.com/ory/hydra/x" ) @@ -14,4 +15,5 @@ type Registry interface { KeyManager() Manager KeyGenerators() map[string]KeyGenerator KeyCipher() *AEAD + HardwareSecurityModule() *crypto11.Context } diff --git a/quickstart-hsm.yml b/quickstart-hsm.yml new file mode 100644 index 00000000000..b0021b6cbc1 --- /dev/null +++ b/quickstart-hsm.yml @@ -0,0 +1,85 @@ +########################################################################### +####### FOR DEMONSTRATION PURPOSES ONLY ####### +########################################################################### +# # +# If you have not yet read the tutorial, do so now: # +# https://www.ory.sh/docs/hydra/5min-tutorial # +# # +# This set up is only for demonstration purposes. The login # +# endpoint can only be used if you follow the steps in the tutorial. # +# # +########################################################################### + +version: '3.7' + +services: + + hydra: + build: + context: . + dockerfile: .docker/Dockerfile-hsm + ports: + - "4444:4444" # Public port + - "4445:4445" # Admin port + - "5555:5555" # Port for hydra token user + command: + serve -c /etc/config/hydra/hydra.yml all --dangerous-force-http + volumes: + - + type: volume + source: hydra-sqlite + target: /var/lib/sqlite + read_only: false + - + type: bind + source: ./contrib/quickstart/5-min + target: /etc/config/hydra + environment: + - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true + - HSM_ENABLED=true + - HSM_LIBRARY=/usr/lib/softhsm/libsofthsm2.so + - HSM_TOKEN_LABEL=hydra + - HSM_PIN=1234 + restart: unless-stopped + depends_on: + - hydra-migrate + networks: + - intranet + + hydra-migrate: + build: + context: . + dockerfile: .docker/Dockerfile-hsm + environment: + - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true + command: + migrate -c /etc/config/hydra/hydra.yml sql -e --yes + volumes: + - + type: volume + source: hydra-sqlite + target: /var/lib/sqlite + read_only: false + - + type: bind + source: ./contrib/quickstart/5-min + target: /etc/config/hydra + restart: on-failure + networks: + - intranet + + consent: + environment: + - HYDRA_ADMIN_URL=http://hydra:4445 + image: oryd/hydra-login-consent-node:v1.10.2 + ports: + - "3000:3000" + restart: unless-stopped + networks: + - intranet + +networks: + intranet: + +volumes: + hydra-sqlite: diff --git a/spec/config.json b/spec/config.json index 3e5faf042b8..ca0b77b8287 100644 --- a/spec/config.json +++ b/spec/config.json @@ -425,6 +425,31 @@ "type": "string", "description": "Sets the data source name. This configures the backend where ORY Hydra persists data. If dsn is \"memory\", data will be written to memory and is lost when you restart this instance. ORY Hydra supports popular SQL databases. For more detailed configuration information go to: https://www.ory.sh/docs/hydra/dependencies-environment#sql" }, + "hsm": { + "type": "object", + "additionalProperties": false, + "description": "Configures Hardware Security Module.", + "properties": { + "enabled": { + "type": "boolean" + }, + "library": { + "type": "string", + "description": "Full pathname (including extension) of the PKCS#11 library" + }, + "pin": { + "type": "string" + }, + "slot": { + "type": "integer", + "description": "Slot of the token to use (if label is not specified)" + }, + "token_label": { + "type": "string", + "description": "Label of the token to use (if slot is not specified)" + } + } + }, "webfinger": { "type": "object", "additionalProperties": false,