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 edgexfoundry#5038. Add key-related APIs in security-proxy-auth to enable support for external JWT verification.

Signed-off-by: Lindsey Cheng <[email protected]>
  • Loading branch information
lindseysimple committed Dec 27, 2024
1 parent 0ef5369 commit b41f93f
Show file tree
Hide file tree
Showing 27 changed files with 1,283 additions and 53 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: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,7 @@ require (
gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
nhooyr.io/websocket v1.8.17 // indirect
)

replace github.com/edgexfoundry/go-mod-core-contracts/v4 => github.com/lindseysimple/go-mod-core-contracts/v4 v4.0.0-20241224070246-4567c6a29d20

replace github.com/edgexfoundry/go-mod-bootstrap/v4 => github.com/lindseysimple/go-mod-bootstrap/v4 v4.0.0-20241226015317-1da7d091abea
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,8 @@ 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.14 h1:C2D1RppHFKME7q/nVJVPHlzLYs12UTkJBz/q/4ZxBTA=
github.com/edgexfoundry/go-mod-bootstrap/v4 v4.0.0-dev.14/go.mod h1:g0H805nWxtzJplM6nFnbTJi1TgXRD730NO9xxhC6xXk=
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.15 h1:4FbSL5rsNXVonrYz4K5v1oCNmi64LvcEx8xCgr6mXOo=
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.0.0-dev.15/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.2 h1:iHu8JPpmrEOrIZdv0iYW69FlMmkyal/FpbXtC3pHt2c=
Expand Down Expand Up @@ -305,6 +301,10 @@ github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lindseysimple/go-mod-bootstrap/v4 v4.0.0-20241226015317-1da7d091abea h1:wv2pZ9XNYvy+npNoWml3k92A80/aTJoksp2Qq0IsvVA=
github.com/lindseysimple/go-mod-bootstrap/v4 v4.0.0-20241226015317-1da7d091abea/go.mod h1:a7rlrr4QTgjNZZGgnikVFGTDIYda1nuyEBQYPJAFD2Q=
github.com/lindseysimple/go-mod-core-contracts/v4 v4.0.0-20241224070246-4567c6a29d20 h1:9AM7b578tXzt7SmvAfNUgyNPZj4PhbbnmsJRGTYmluU=
github.com/lindseysimple/go-mod-core-contracts/v4 v4.0.0-20241224070246-4567c6a29d20/go.mod h1:M5JXcRrmnIVNAmqeDNVXd0PSOGdq96fgrEmzivx02c8=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
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
}
105 changes: 105 additions & 0 deletions internal/pkg/utils/aes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//
// Copyright (C) 2024 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package utils

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

"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() *AESCryptor {
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/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 utils

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 b41f93f

Please sign in to comment.