Skip to content

Commit

Permalink
Hardware Security Module support for keys hydra.openid.id-token, hydr…
Browse files Browse the repository at this point in the history
…a.jwt.access-token
  • Loading branch information
aarmam committed Jul 30, 2021
1 parent 4083684 commit 832f8af
Show file tree
Hide file tree
Showing 17 changed files with 480 additions and 68 deletions.
56 changes: 56 additions & 0 deletions .docker/Dockerfile-hsm
Original file line number Diff line number Diff line change
@@ -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"]
91 changes: 91 additions & 0 deletions docs/docs/hsm-support.md
Original file line number Diff line number Diff line change
@@ -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
```
5 changes: 5 additions & 0 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions docs/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<value>
# $ export HSM_PIN=<value>
# $ export HSM_SLOT=<value>
# $ export HSM_TOKEN_LABEL=<value>
# - Windows Command Line (CMD):
# > set HSM_ENABLED=<value>
# > set HSM_PIN=<value>
# > set HSM_SLOT=<value>
# > set HSM_TOKEN_LABEL=<value>
#
hsm:
enabled: false
library: /path/to/hsm-vendor/library.so
pin: partition-pin-code
slot: 0
token_label: hydra

## webfinger ##
#
# Configures ./well-known/ settings.
Expand Down
26 changes: 26 additions & 0 deletions driver/config/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
61 changes: 52 additions & 9 deletions driver/registry_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package driver

import (
"context"
"crypto/rsa"
"fmt"
"github.com/ThalesIgnite/crypto11"
"net"
"net/http"
"strings"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}
1 change: 1 addition & 0 deletions driver/registry_base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func TestRegistryBase_newKeyStrategy_handlesNetworkError(t *testing.T) {
}

registryBase := RegistryBase{r: registry, l: l}
registryBase.WithConfig(c)

strategy := registryBase.newKeyStrategy("key")

Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
Loading

0 comments on commit 832f8af

Please sign in to comment.