Files
threshold/internal/auth/oidc/service.go
T
hashicorp-copywrite[bot]andJohan Brandhorst-Satzkorn 29da0bcb92 [COMPLIANCE] License changes (#3567)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* Update copyright file headers to BUS-1.1

* Rerun make gen

This will pick up the last of the license changes

* Revert "Rerun make gen"

This reverts commit dbe0968bffd92745d8663c6940debae098395a6b.

* Remove copywrite from generation script

The current version of copywrite is not sophisticated enough
to only apply the copywrite changes to certain subdirectories,
so to avoid accidentally relicensing our api and sdk modules,
we will stop running the copywrite script until such time
that it can do the right thing.

* License protobuf API as MPL

The protobuf API files are used to generate the SDK,
since we want the SDK to be MPL, the protobuf files
must also be MPL. This extends to all protobuf files
whose sources end up in the SDK directory, including:

- internal/proto/controller/api
- internal/proto/controller/custom_options
- internal/proto/plugin

It also includes the generated service directory in
internal/gen/controller/api/services, since they are
generated from the protobuf sources used for the SDK.

* Change website license file name

This makes the naming scheme consistent with the rest
of the repository.

* Add note on licensing to README

* Set copywrite license to BUSL

* Revert "Add note on licensing to README"

This reverts commit 4d865a91a7a4f11605f0dc4609355d7e3c3cfffe.

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
2023-08-11 10:31:53 -07:00

249 lines
9.6 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package oidc
import (
"context"
"crypto/ed25519"
"fmt"
"time"
"github.com/hashicorp/boundary/internal/auth/oidc/request"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/libs/crypto"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
aead "github.com/hashicorp/go-kms-wrapping/v2/aead"
"github.com/mr-tron/base58"
"google.golang.org/protobuf/proto"
)
const (
// AttemptExpiration defines the TTL for an authentication attempt
AttemptExpiration = 5 * 60 * time.Second
// FinalRedirectEndpoint is the endpoint that the oidc callback redirect
// client to after the callback is complete.
FinalRedirectEndpoint = "%s/authentication-complete"
// AuthenticationErrorsEndpoint is the endpoint that will returned as the final redirect
// from the callback when there are auth errors
AuthenticationErrorsEndpoint = "%s/authentication-error"
// CallbackEndpoint is the endpoint for the oidc callback which will be
// included in the auth URL returned when an authen attempted is kicked off.
CallbackEndpoint = "%s/v1/auth-methods/oidc:authenticate:callback"
)
type (
// OidcRepoFactory is used by "service functions" to create a new oidc repo
OidcRepoFactory func() (*Repository, error)
// IamRepoFactory is used by "service functions" to create a new iam repo
IamRepoFactory func() (*iam.Repository, error)
// AuthTokenRepoFactory is used by "service functions" to create a new auth token repo
AuthTokenRepoFactory func() (*authtoken.Repository, error)
)
// validator defines an optional interface that proto messages can implement
// which will allow encryptMessage to validate them before encryption.
type validator interface {
Validate() error
}
// encryptMessage will encrypt the message. The encrypted message will be wrapped in a
// request.Wrapper and then encoded into the returned string. This function
// supports encrypting: request.State and request.Token messages. proto Messages should
// implement the validator interface, so they can be validated before encryption.
func encryptMessage(ctx context.Context, wrapper wrapping.Wrapper, am *AuthMethod, m proto.Message) (encodedEncryptedState string, e error) {
const op = "oidc.encryptMessage"
if wrapper == nil {
return "", errors.New(ctx, errors.InvalidParameter, op, "missing wrapper")
}
keyId, err := wrapper.KeyId(ctx)
if err != nil {
return "", errors.Wrap(ctx, err, op, errors.WithMsg("error fetching wrapper key id"))
}
if keyId == "" {
return "", errors.New(ctx, errors.InvalidParameter, op, "missing wrapper key id")
}
if am == nil || am.AuthMethod == nil {
return "", errors.New(ctx, errors.InvalidParameter, op, "missing auth method")
}
if am.ScopeId == "" {
return "", errors.New(ctx, errors.InvalidParameter, op, "missing auth method scope id")
}
if am.PublicId == "" {
return "", errors.New(ctx, errors.InvalidParameter, op, "missing auth method public id")
}
if m == nil {
return "", errors.New(ctx, errors.InvalidParameter, op, "missing message to encrypt")
}
switch v := m.(type) {
case *request.State, *request.Token:
default:
return "", errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported message type %v for encryption", v))
}
v, ok := m.(validator)
if ok {
if err := v.Validate(); err != nil {
return "", errors.Wrap(ctx, err, op)
}
}
marshaled, err := proto.Marshal(m)
if err != nil {
return "", errors.Wrap(ctx, err, op, errors.WithMsg("unable to marshal message"), errors.WithCode(errors.Encode))
}
blobInfo, err := wrapper.Encrypt(ctx, marshaled, wrapping.WithAad([]byte(fmt.Sprintf("%s%s", am.PublicId, am.ScopeId))))
if err != nil {
return "", errors.New(ctx, errors.Encrypt, op, "unable to encrypt message", errors.WithWrap(err))
}
marshaledBlob, err := proto.Marshal(blobInfo)
if err != nil {
return "", errors.Wrap(ctx, err, op, errors.WithMsg("unable to marshal blob"), errors.WithCode(errors.Encode))
}
wrapped := &request.Wrapper{
AuthMethodId: am.PublicId,
ScopeId: am.ScopeId,
WrapperKeyId: keyId,
Ct: marshaledBlob,
}
if err := wrapped.Validate(ctx); err != nil {
return "", errors.Wrap(ctx, err, op)
}
marshaledEncryptedSt, err := proto.Marshal(wrapped)
if err != nil {
return "", errors.New(ctx, errors.Unknown, op, "unable to marshal encrypted message", errors.WithWrap(err))
}
return base58.FastBase58Encoding(marshaledEncryptedSt), nil
}
// decryptMessage will decrypt messages that were previously encrypted with oidc.encryptMessage(...)
// The returned messageBytes can be marshaled via proto.Marshal into the appropriate message.
func decryptMessage(ctx context.Context, wrappingWrapper wrapping.Wrapper, wrappedRequest *request.Wrapper) (messageBytes []byte, e error) {
const op = "oidc.decryptMessage"
if wrappedRequest == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing wrapped request")
}
if wrappingWrapper == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing wrapping wrapper")
}
var blobInfo wrapping.BlobInfo
if err := proto.Unmarshal(wrappedRequest.Ct, &blobInfo); err != nil {
return nil, errors.New(ctx, errors.Unknown, op, "unable to marshal blob info", errors.WithWrap(err))
}
decryptedMsg, err := wrappingWrapper.Decrypt(ctx, &blobInfo, wrapping.WithAad([]byte(fmt.Sprintf("%s%s", wrappedRequest.AuthMethodId, wrappedRequest.ScopeId))))
if err != nil {
return nil, errors.New(ctx, errors.Decrypt, op, "unable to decrypt message", errors.WithWrap(err))
}
return decryptedMsg, nil
}
// UnwrapMessage does just that, it unwraps the encoded request.Wrapper proto message
func UnwrapMessage(ctx context.Context, encodedWrappedMsg string) (*request.Wrapper, error) {
const op = ""
decoded, err := base58.FastBase58Decoding(encodedWrappedMsg)
if err != nil {
return nil, errors.New(ctx, errors.Unknown, op, "unable to decode message", errors.WithWrap(err))
}
var wrapper request.Wrapper
if err := proto.Unmarshal(decoded, &wrapper); err != nil {
return nil, errors.New(ctx, errors.Unknown, op, "unable to marshal encoded/encrypted message", errors.WithWrap(err))
}
return &wrapper, nil
}
// requestWrappingWrapper finds the wrapping wrapper to use when encrypting/decrypting
// both a Request.State and Request.Token.
//
// It first checks the cache of derived wrappers. If it's not found in the cache, it
// generates a key based on the scope's oidc DEK, using the scopeId and authMethod
// as salt and info for derivation, creates a wrapper for the new key, adds that wrapper
// to the cache and the returns the wrapper for the new key.
//
// It supports the WithKeyId(...) option which allows you to specify which oidc DEK
// to use vs the default of just using the latest version of the DEK.
func requestWrappingWrapper(ctx context.Context, k *kms.Kms, scopeId, authMethodId string, opt ...Option) (wrapping.Wrapper, error) {
const op = "oidc.(Repository).oidcWrapper"
if k == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing kms")
}
if scopeId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
}
if authMethodId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
}
opts := getOpts(opt...)
// get a specific oidcWrapper using the WithKeyId(...) option
oidcWrapper, err := k.GetWrapper(ctx, scopeId, kms.KeyPurposeOidc, kms.WithKeyId(opts.withKeyId))
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oidc wrapper"))
}
// What derived key are we looking for?
keyId, err := oidcWrapper.KeyId(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oidc wrapper key id"))
}
keyId = derivedKeyId(derivedKeyPurposeState, keyId, authMethodId)
derivedWrapper, ok := k.GetDerivedPurposeCache().Load(keyId)
if ok {
return derivedWrapper.(*aead.Wrapper), nil
}
// okay, I guess we need to derive a new key for this combo of oidcWrapper and authMethod
reader, err := crypto.NewDerivedReader(ctx, oidcWrapper, 32, []byte(authMethodId), []byte(scopeId))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
privKey, _, err := ed25519.GenerateKey(reader)
if err != nil {
return nil, errors.New(ctx, errors.Encrypt, op, "unable to generate key", errors.WithWrap(err))
}
wrapper := aead.NewWrapper()
if _, err := wrapper.SetConfig(ctx, wrapping.WithKeyId(keyId)); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("error setting config on aead wrapper in auth method %s", authMethodId)))
}
if err := wrapper.SetAesGcmKeyBytes(privKey); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("error setting key bytes on aead wrapper in auth method %s", authMethodId)))
}
// store the derived key in our cache
k.GetDerivedPurposeCache().Store(keyId, wrapper)
return wrapper, nil
}
// derivedKeyId returns a key that represents the derived key
func derivedKeyId(purpose derivedKeyPurpose, wrapperKeyId, authMethodId string) string {
return fmt.Sprintf("%s.%s.%s", purpose.String(), wrapperKeyId, authMethodId)
}
// derivedKeyPurpose represents the purpose of the derived key.
//
// In the future, this could be moved to the kms package
// and exported for others to use.
type derivedKeyPurpose uint
const (
derivedKeyPurposeUnknown = iota
derivedKeyPurposeState
)
// String returns a representative string for the key's purpose
func (k derivedKeyPurpose) String() string {
switch k {
case derivedKeyPurposeState:
return "oidc_state"
default:
return "oidc_unknown"
}
}