Skip to content

Commit

Permalink
feat: Add key-related APIs in security-proxy-auth
Browse files Browse the repository at this point in the history
Resolves #5038. Add key-related APIs in security-proxy-auth to enable support for external JWT verification.

Signed-off-by: Lindsey Cheng <beckysocute@gmail.com>
  • Loading branch information
lindseysimple committed Jan 7, 2025
1 parent b24805f commit 470e743
Show file tree
Hide file tree
Showing 35 changed files with 1,934 additions and 54 deletions.
6 changes: 6 additions & 0 deletions cmd/core-common-config-bootstrapper/res/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ all-services:
DefaultPubRetryAttempts: "2"
Subject: "edgex/#" # Required for NATS JetStream only for stream auto-provisioning

Clients:
security-proxy-auth:
Protocol: http
Host: localhost
Port: 59842

app-services:
Writable:
StoreAndForward:
Expand Down
7 changes: 7 additions & 0 deletions cmd/security-proxy-auth/res/db/sql/00-utils.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
--
-- Copyright (C) 2024 IOTech Ltd
--
-- SPDX-License-Identifier: Apache-2.0

-- schema for proxy-auth related tables
CREATE SCHEMA IF NOT EXISTS security_proxy_auth;
13 changes: 13 additions & 0 deletions cmd/security-proxy-auth/res/db/sql/01-tables.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--
-- Copyright (C) 2024 IOTech Ltd
--
-- SPDX-License-Identifier: Apache-2.0

-- security_proxy_auth.key_store is used to store the key file
CREATE TABLE IF NOT EXISTS security_proxy_auth.key_store (
id UUID PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
content TEXT NOT NULL,
created timestamp NOT NULL DEFAULT (now() AT TIME ZONE 'utc'),
modified timestamp NOT NULL DEFAULT (now() AT TIME ZONE 'utc')
);
3 changes: 3 additions & 0 deletions cmd/security-secretstore-setup/res/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ Databases:
scheduler:
Service: support-scheduler
Username: support_scheduler
securityproxyauth:
Service: security-proxy-auth
Username: security_proxy_auth
SecureMessageBus:
Type: none
KuiperConfigPath: /tmp/kuiper/edgex.yaml
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ go 1.23

require (
github.com/eclipse/paho.mqtt.golang v1.5.0
github.com/edgexfoundry/go-mod-bootstrap/v4 v4.0.0-dev.15
github.com/edgexfoundry/go-mod-bootstrap/v4 v4.0.0-dev.16
github.com/edgexfoundry/go-mod-configuration/v4 v4.0.0-dev.10
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.0.0-dev.18
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.0.0-dev.19
github.com/edgexfoundry/go-mod-messaging/v4 v4.0.0-dev.10
github.com/edgexfoundry/go-mod-secrets/v4 v4.0.0-dev.5
github.com/fxamacker/cbor/v2 v2.7.0
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o=
github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk=
github.com/edgexfoundry/go-mod-bootstrap/v4 v4.0.0-dev.15 h1:oeSDtoah8q3sBo8huqNdRjjxRF5IcsLh0kby0gJW/o4=
github.com/edgexfoundry/go-mod-bootstrap/v4 v4.0.0-dev.15/go.mod h1:D+fSf0PWO9E4nz+1tVe0OGYnBeRQ1nHdF3B1tnYrq60=
github.com/edgexfoundry/go-mod-bootstrap/v4 v4.0.0-dev.16 h1:NSEoo/YnSDNWQcZpLAtXqDKHAId5mecTVBxk9zs/ROg=
github.com/edgexfoundry/go-mod-bootstrap/v4 v4.0.0-dev.16/go.mod h1:66I+qRA22YkjA/SYw4F9R0avZ9oNYM8bg2qhwePTFkI=
github.com/edgexfoundry/go-mod-configuration/v4 v4.0.0-dev.10 h1:DMv5LZDxcqUeb1dREMd/vK+reXmZYlpafgtm8XhYdHQ=
github.com/edgexfoundry/go-mod-configuration/v4 v4.0.0-dev.10/go.mod h1:ltUpMcOpJSzmabBtZox5qg1AK2wEikvZJyIBXtJ7mUQ=
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.0.0-dev.18 h1:yGXoMQd1XFM924NYukAagbZ4xLYFCzMg0bjGWZtStCM=
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.0.0-dev.18/go.mod h1:M5JXcRrmnIVNAmqeDNVXd0PSOGdq96fgrEmzivx02c8=
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.0.0-dev.19 h1:uXZml7n/I/+c7k3eZRuJRrlWjYx/Euk8tlnBqeitvB8=
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.0.0-dev.19/go.mod h1:M5JXcRrmnIVNAmqeDNVXd0PSOGdq96fgrEmzivx02c8=
github.com/edgexfoundry/go-mod-messaging/v4 v4.0.0-dev.10 h1:xvDQDIJtmj/ZCmKzbAzg3h1F2ZdWz1MPoJSNfYZANGc=
github.com/edgexfoundry/go-mod-messaging/v4 v4.0.0-dev.10/go.mod h1:ibaiw7r3RgLYDuuFfWT1kh//bjP+onDOOQsnSsdD4E8=
github.com/edgexfoundry/go-mod-registry/v4 v4.0.0-dev.3 h1:6tw6JqEJDOqo2lEgxjZ+scvsub5R20WGpInCuoxS6zE=
Expand Down
7 changes: 2 additions & 5 deletions internal/core/command/router.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2021-2023 IOTech Ltd
// Copyright (C) 2021-2024 IOTech Ltd
// Copyright (C) 2023 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
Expand All @@ -9,7 +9,6 @@ package command
import (
"github.com/edgexfoundry/edgex-go"
commandController "github.com/edgexfoundry/edgex-go/internal/core/command/controller/http"
"github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container"
"github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/controller"
"github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/handlers"
"github.com/edgexfoundry/go-mod-bootstrap/v4/di"
Expand All @@ -19,9 +18,7 @@ import (
)

func LoadRestRoutes(r *echo.Echo, dic *di.Container, serviceName string) {
lc := container.LoggingClientFrom(dic.Get)
secretProvider := container.SecretProviderExtFrom(dic.Get)
authenticationHook := handlers.AutoConfigAuthenticationFunc(secretProvider, lc)
authenticationHook := handlers.AutoConfigAuthenticationFunc(dic)

// Common
_ = controller.NewCommonController(dic, r, serviceName, edgex.Version)
Expand Down
7 changes: 2 additions & 5 deletions internal/core/data/router.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2021-2023 IOTech Ltd
// Copyright (C) 2021-2024 IOTech Ltd
// Copyright (C) 2023 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
Expand All @@ -8,7 +8,6 @@ package data

import (
"github.com/edgexfoundry/edgex-go"
"github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container"
"github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/controller"
"github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/handlers"
"github.com/edgexfoundry/go-mod-bootstrap/v4/di"
Expand All @@ -20,9 +19,7 @@ import (
)

func LoadRestRoutes(r *echo.Echo, dic *di.Container, serviceName string) {
lc := container.LoggingClientFrom(dic.Get)
secretProvider := container.SecretProviderExtFrom(dic.Get)
authenticationHook := handlers.AutoConfigAuthenticationFunc(secretProvider, lc)
authenticationHook := handlers.AutoConfigAuthenticationFunc(dic)

// Common
_ = controller.NewCommonController(dic, r, serviceName, edgex.Version)
Expand Down
5 changes: 1 addition & 4 deletions internal/core/keeper/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package keeper

import (
"github.com/edgexfoundry/edgex-go"
"github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container"
"github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/controller"
"github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/handlers"
"github.com/edgexfoundry/go-mod-bootstrap/v4/di"
Expand All @@ -19,9 +18,7 @@ import (
)

func LoadRestRoutes(r *echo.Echo, dic *di.Container, serviceName string) {
lc := container.LoggingClientFrom(dic.Get)
secretProvider := container.SecretProviderExtFrom(dic.Get)
authenticationHook := handlers.AutoConfigAuthenticationFunc(secretProvider, lc)
authenticationHook := handlers.AutoConfigAuthenticationFunc(dic)

// Common
_ = controller.NewCommonController(dic, r, serviceName, edgex.Version)
Expand Down
5 changes: 1 addition & 4 deletions internal/core/metadata/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ package metadata

import (
"github.com/edgexfoundry/edgex-go"
"github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container"
"github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/controller"
"github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/handlers"
"github.com/edgexfoundry/go-mod-bootstrap/v4/di"
Expand All @@ -21,9 +20,7 @@ import (
)

func LoadRestRoutes(r *echo.Echo, dic *di.Container, serviceName string) {
lc := container.LoggingClientFrom(dic.Get)
secretProvider := container.SecretProviderExtFrom(dic.Get)
authenticationHook := handlers.AutoConfigAuthenticationFunc(secretProvider, lc)
authenticationHook := handlers.AutoConfigAuthenticationFunc(dic)

// Common
_ = controller.NewCommonController(dic, r, serviceName, edgex.Version)
Expand Down
3 changes: 3 additions & 0 deletions internal/pkg/infrastructure/postgres/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const (
coreMetaDataSchema = "core_metadata"
supportNotificationsSchema = "support_notifications"
supportSchedulerSchema = "support_scheduler"
ProxyAuthSchema = "security_proxy_auth"
)

// constants relate to the postgres db table names
Expand All @@ -29,6 +30,7 @@ const (
scheduleJobTableName = supportSchedulerSchema + ".job"
subscriptionTableName = supportNotificationsSchema + ".subscription"
transmissionTableName = supportNotificationsSchema + ".transmission"
keyStoreTableName = ProxyAuthSchema + ".key_store"
)

// constants relate to the common db table column names
Expand All @@ -38,6 +40,7 @@ const (
idCol = "id"
modifiedCol = "modified"
statusCol = "status"
nameCol = "name"
)

// constants relate to the event/reading postgres db table column names
Expand Down
67 changes: 67 additions & 0 deletions internal/pkg/infrastructure/postgres/keystore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// Copyright (C) 2024 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package postgres

import (
"context"
"fmt"
"time"

pgClient "github.com/edgexfoundry/edgex-go/internal/pkg/db/postgres"

"github.com/edgexfoundry/go-mod-core-contracts/v4/errors"

"github.com/google/uuid"
)

// AddKey adds a new key to the database
func (c *Client) AddKey(name, content string) errors.EdgeX {
exists, err := c.KeyExists(name)
if err != nil {
return errors.NewCommonEdgeXWrapper(err)
} else if exists {
return errors.NewCommonEdgeX(errors.KindDuplicateName, fmt.Sprintf("key '%s' already exists", name), nil)
}

_, pgxErr := c.ConnPool.Exec(
context.Background(), sqlInsert(keyStoreTableName, idCol, nameCol, contentCol),
uuid.New().String(), name, content)
if pgxErr != nil {
return pgClient.WrapDBError("failed to insert row to key_store table", pgxErr)
}
return nil
}

// UpdateKey updates the key by name
func (c *Client) UpdateKey(name, content string) errors.EdgeX {
_, pgxErr := c.ConnPool.Exec(
context.Background(), sqlUpdateColsByCondCol(keyStoreTableName, nameCol, contentCol, modifiedCol), content, time.Now().UTC(), name)
if pgxErr != nil {
return pgClient.WrapDBError("failed to update row to key_store table", pgxErr)
}
return nil
}

// ReadKeyContent reads key content from the database
func (c *Client) ReadKeyContent(name string) (string, errors.EdgeX) {
var fileContent string
row := c.ConnPool.QueryRow(context.Background(),
fmt.Sprintf("SELECT %s FROM %s WHERE %s = $1", contentCol, keyStoreTableName, nameCol), name)
if err := row.Scan(&fileContent); err != nil {
return fileContent, pgClient.WrapDBError("failed to query key content", err)
}
return fileContent, nil
}

// KeyExists check whether the key file exits
func (c *Client) KeyExists(filename string) (bool, errors.EdgeX) {
var exists bool
err := c.ConnPool.QueryRow(context.Background(), sqlCheckExistsByCol(keyStoreTableName, nameCol), filename).Scan(&exists)
if err != nil {
return false, pgClient.WrapDBError(fmt.Sprintf("failed to check key by name '%s' from %s table", nameCol, keyStoreTableName), err)
}
return exists, nil
}
107 changes: 107 additions & 0 deletions internal/pkg/utils/crypto/aes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//
// Copyright (C) 2024 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package crypto

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"io"

"github.com/edgexfoundry/edgex-go/internal/pkg/utils/crypto/interfaces"

"github.com/edgexfoundry/go-mod-core-contracts/v4/errors"
)

const aesKey = "6NKEwDmK9Jz2aa6IqcTS6jNlA3mQP53B"

// AESCryptor defined the AES cryptor struct
type AESCryptor struct {
key []byte
}

func NewAESCryptor() interfaces.Crypto {
return &AESCryptor{
key: []byte(aesKey),
}
}

// Encrypt encrypts the given plaintext with AES-CBC mode and returns a string in base64 encoding
func (c *AESCryptor) Encrypt(plaintext string) (string, errors.EdgeX) {
bytePlaintext := []byte(plaintext)
block, err := aes.NewCipher(c.key)
if err != nil {
return "", errors.NewCommonEdgeX(errors.KindServerError, "encrypt failed", err)
}

// CBC mode works on blocks so plaintexts may need to be padded to the next whole block
paddedPlaintext := pkcs7Pad(bytePlaintext, block.BlockSize())

ciphertext := make([]byte, aes.BlockSize+len(paddedPlaintext))
// attach a random iv ahead of the ciphertext
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", errors.NewCommonEdgeX(errors.KindServerError, "encrypt failed", err)
}

mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext[aes.BlockSize:], paddedPlaintext)

return base64.StdEncoding.EncodeToString(ciphertext), nil
}

// Decrypt decrypts the given ciphertext with AES-CBC mode and returns the original value as string
func (c *AESCryptor) Decrypt(ciphertext string) ([]byte, errors.EdgeX) {
decodedCipherText, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err)
}

block, err := aes.NewCipher(c.key)
if err != nil {
return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err)
}

if len(decodedCipherText) < aes.BlockSize {
return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err)
}

// get the iv from the cipher text
iv := decodedCipherText[:aes.BlockSize]
decodedCipherText = decodedCipherText[aes.BlockSize:]

mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(decodedCipherText, decodedCipherText)

// If the original plaintext lengths are not a multiple of the block
// size, padding would have to be added when encrypting, which would be
// removed at this point
plaintext, e := pkcs7Unpad(decodedCipherText)
if e != nil {
return nil, errors.NewCommonEdgeXWrapper(err)
}

return plaintext, nil
}

// pkcs7Pad implements the PKCS7 padding
func pkcs7Pad(data []byte, blockSize int) []byte {
padding := blockSize - (len(data) % blockSize)
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(data, padText...)
}

// pkcs7Unpad implements the PKCS7 unpadding
func pkcs7Unpad(data []byte) ([]byte, errors.EdgeX) {
length := len(data)
unpadding := int(data[length-1])
if unpadding > length {
return nil, errors.NewCommonEdgeX(errors.KindServerError, "invalid padding", nil)
}
return data[:(length - unpadding)], nil
}
24 changes: 24 additions & 0 deletions internal/pkg/utils/crypto/aes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// Copyright (C) 2024 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package crypto

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestAESCryptor_Encryption(t *testing.T) {
testData := "test data"
aesCryptor := NewAESCryptor()

encrypted, err := aesCryptor.Encrypt(testData)
require.NoError(t, err)

decrypted, err := aesCryptor.Decrypt(encrypted)
require.NoError(t, err)
require.Equal(t, testData, string(decrypted))
}
Loading

0 comments on commit 470e743

Please sign in to comment.