Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEOS-1612: adds separate public key pair for neosync cloud checks #2992

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ venv2/
python/dist/

neosync_ee_ca.key
neosync_cloud_ca.key
29 changes: 14 additions & 15 deletions backend/internal/cmds/mgmt/serve/connect/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import (
v1alpha1_useraccountservice "github.com/nucleuscloud/neosync/backend/services/mgmt/v1alpha1/user-account-service"
awsmanager "github.com/nucleuscloud/neosync/internal/aws"
"github.com/nucleuscloud/neosync/internal/billing"
cloudlicense "github.com/nucleuscloud/neosync/internal/ee/cloud-license"
"github.com/nucleuscloud/neosync/internal/ee/license"
presidioapi "github.com/nucleuscloud/neosync/internal/ee/presidio"
neomigrate "github.com/nucleuscloud/neosync/internal/migrate"
Expand Down Expand Up @@ -115,9 +116,11 @@ func serve(ctx context.Context) error {
}
slogger.Debug(fmt.Sprintf("ee license enabled: %t", eelicense.IsValid()))

if getIsNeosyncCloud() {
slogger.Debug("neosync cloud is enabled")
ncloudlicense, err := cloudlicense.NewFromEnv()
if err != nil {
return err
}
slogger.Debug(fmt.Sprintf("neosync cloud enabled: %t", ncloudlicense.IsValid()))

mux := http.NewServeMux()

Expand Down Expand Up @@ -301,7 +304,7 @@ func serve(ctx context.Context) error {
if err != nil {
return err
}
apikeyClient := auth_apikey.New(db.Q, db.Db, getAllowedWorkerApiKeys(getIsNeosyncCloud()), []string{
apikeyClient := auth_apikey.New(db.Q, db.Db, getAllowedWorkerApiKeys(ncloudlicense.IsValid()), []string{
mgmtv1alpha1connect.JobServiceGetJobProcedure,
mgmtv1alpha1connect.JobServiceGetRunContextProcedure,
mgmtv1alpha1connect.JobServiceSetRunContextProcedure,
Expand Down Expand Up @@ -431,7 +434,7 @@ func serve(ctx context.Context) error {

useraccountService := v1alpha1_useraccountservice.New(&v1alpha1_useraccountservice.Config{
IsAuthEnabled: isAuthEnabled,
IsNeosyncCloud: getIsNeosyncCloud(),
IsNeosyncCloud: ncloudlicense.IsValid(),
DefaultMaxAllowedRecords: getDefaultMaxAllowedRecords(),
}, db, temporalConfigProvider, authclient, authadminclient, billingClient)
api.Handle(
Expand Down Expand Up @@ -480,7 +483,7 @@ func serve(ctx context.Context) error {
)

jobhookOpts := []jobhooks.Option{}
if getIsNeosyncCloud() || eelicense.IsValid() {
if ncloudlicense.IsValid() || eelicense.IsValid() {
jobhookOpts = append(jobhookOpts, jobhooks.WithEnabled())
}

Expand All @@ -497,7 +500,7 @@ func serve(ctx context.Context) error {

jobServiceConfig := &v1alpha1_jobservice.Config{
IsAuthEnabled: isAuthEnabled,
IsNeosyncCloud: getIsNeosyncCloud(),
IsNeosyncCloud: ncloudlicense.IsValid(),
RunLogConfig: runLogConfig,
}
jobService := v1alpha1_jobservice.New(
Expand All @@ -522,7 +525,7 @@ func serve(ctx context.Context) error {
var presAnalyzeClient presidioapi.AnalyzeInterface
var presAnonClient presidioapi.AnonymizeInterface
var presEntityClient presidioapi.EntityInterface
if getIsNeosyncCloud() {
if ncloudlicense.IsValid() {
analyzeClient, ok, err := getPresidioAnalyzeClient()
if err != nil {
return fmt.Errorf("unable to initialize presidio analyze client: %w", err)
Expand All @@ -543,8 +546,8 @@ func serve(ctx context.Context) error {
}

transformerService := v1alpha1_transformerservice.New(&v1alpha1_transformerservice.Config{
IsPresidioEnabled: getIsNeosyncCloud(),
IsNeosyncCloud: getIsNeosyncCloud(),
IsPresidioEnabled: ncloudlicense.IsValid(),
IsNeosyncCloud: ncloudlicense.IsValid(),
}, db, useraccountService, presEntityClient)
api.Handle(
mgmtv1alpha1connect.NewTransformersServiceHandler(
Expand All @@ -557,9 +560,9 @@ func serve(ctx context.Context) error {
)

anonymizationService := v1alpha1_anonymizationservice.New(&v1alpha1_anonymizationservice.Config{
IsPresidioEnabled: getIsNeosyncCloud(),
IsPresidioEnabled: ncloudlicense.IsValid(),
IsAuthEnabled: isAuthEnabled,
IsNeosyncCloud: getIsNeosyncCloud(),
IsNeosyncCloud: ncloudlicense.IsValid(),
}, anonymizerMeter, useraccountService, presAnalyzeClient, presAnonClient, db)
api.Handle(
mgmtv1alpha1connect.NewAnonymizationServiceHandler(
Expand Down Expand Up @@ -891,10 +894,6 @@ func getAuthApiProvider() string {
return viper.GetString("AUTH_API_PROVIDER")
}

func getIsNeosyncCloud() bool {
return viper.GetBool("NEOSYNC_CLOUD")
}

func getAllowedWorkerApiKeys(isNeosyncCloud bool) []string {
if isNeosyncCloud {
return viper.GetStringSlice("NEOSYNC_CLOUD_ALLOWED_WORKER_API_KEYS")
Expand Down
144 changes: 144 additions & 0 deletions internal/ee/cloud-license/license.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package cloudlicense

import (
"crypto/ed25519"
"crypto/x509"
_ "embed"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"time"

"github.com/spf13/viper"
)

//go:embed neosync_cloud_pub.pem
var publicKeyPEM string

// The expected base64 decoded structure of the NEOSYNC_CLOUD_LICENSE file
type licenseFile struct {
License string `json:"license"`
Signature string `json:"signature"`
}

type Interface interface {
IsValid() bool
}

var _ Interface = (*CloudLicense)(nil)

type CloudLicense struct {
contents *licenseContents
}

// Determines if Neosync Cloud is enabled.
// If not enabled, returns a valid struct where IsValid returns false
// If enabled but no license if provided, returns an error
func NewFromEnv() (*CloudLicense, error) {
lc, isEnabled, err := getFromEnv()
if err != nil {
return nil, err
}
if !isEnabled {
return &CloudLicense{contents: nil}, nil
}
return &CloudLicense{contents: lc}, nil
}

func (c *CloudLicense) IsValid() bool {
return c.contents != nil && c.contents.IsValid()
}

type licenseContents struct {
Version string `json:"version"`
Id string `json:"id"`
IssuedTo string `json:"issued_to"`
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
}

func (l *licenseContents) IsValid() bool {
return time.Now().UTC().Before(l.ExpiresAt)
}

const (
cloudLicenseEvKey = "NEOSYNC_CLOUD_LICENSE"
cloudEnabledEvKey = "NEOSYNC_CLOUD"
)

func getFromEnv() (*licenseContents, bool, error) {
isCloud := viper.GetBool(cloudEnabledEvKey)
if !isCloud {
return nil, false, nil
}

input := viper.GetString(cloudLicenseEvKey)
if input == "" {
return nil, false, fmt.Errorf("%s was true but no license was found at %s", cloudEnabledEvKey, cloudLicenseEvKey)
}
pk, err := parsePublicKey(publicKeyPEM)
if err != nil {
return nil, false, fmt.Errorf("unable to parse neosync cloud public key: %w", err)
}
contents, err := getLicense(input, pk)
if err != nil {
return nil, false, fmt.Errorf("failed to parse provided license: %w", err)
}
return contents, true, nil
}

// Expected the license data to be a base64 encoded json string that matches the licenseFile structure.
func getLicense(licenseData string, publicKey ed25519.PublicKey) (*licenseContents, error) {
licenseDataContents, err := base64.StdEncoding.DecodeString(licenseData)
if err != nil {
return nil, fmt.Errorf("unable to decode license data: %w", err)
}

var license licenseFile
err = json.Unmarshal(licenseDataContents, &license)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal license data from input: %w", err)
}
contents, err := base64.StdEncoding.DecodeString(license.License)
if err != nil {
return nil, fmt.Errorf("unable to decode contents: %w", err)
}
signature, err := base64.StdEncoding.DecodeString(license.Signature)
if err != nil {
return nil, fmt.Errorf("unable to decode signature: %w", err)
}

ok := ed25519.Verify(publicKey, contents, signature)
if !ok {
return nil, errors.New("unable to verify contents against public key")
}

var lc licenseContents
err = json.Unmarshal(contents, &lc)
if err != nil {
return nil, fmt.Errorf("contents verified, but unable to unmarshal license contents from input: %w", err)
}

return &lc, nil
}

func parsePublicKey(data string) (ed25519.PublicKey, error) {
block, _ := pem.Decode([]byte(data))
if block == nil {
return nil, errors.New("failed to parse PEM block containing the public key")
}

pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse DER encoded public key: %v", err)
}

switch pub := pub.(type) {
case ed25519.PublicKey:
return pub, nil
default:
return nil, fmt.Errorf("unsupported public key: %T", pub)
}
}
72 changes: 72 additions & 0 deletions internal/ee/cloud-license/license_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cloudlicense

import (
"testing"
"time"

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

func Test_parsePublicKey(t *testing.T) {
t.Run("empty", func(t *testing.T) {
actual, err := parsePublicKey("")
require.Error(t, err)
require.Nil(t, actual)
})
t.Run("invalid format", func(t *testing.T) {
actual, err := parsePublicKey("blah")
require.Error(t, err)
require.Nil(t, actual)
})
t.Run("valid", func(t *testing.T) {
actual, err := parsePublicKey(publicKeyPEM)
require.NoError(t, err)
require.NotNil(t, actual)
})
}

const (
// generated using the gen-cust-license shell script with the neosync cloud private key
// ./scripts/gen-cust-license.sh ./neosync_cloud_ca.key license.json | pbcopy
validExpiredTestLicense = "eyJsaWNlbnNlIjoiZXdvZ0lDQWdJblpsY25OcGIyNGlPaUFpZGpFaUxBb2dJQ0FnSW1sa0lqb2dJbVk0TW1aaVlXWmtMVFppTnpVdE5HSXpaUzFoWmpRekxUZGhaRFF3TldNNFpEUTRZaUlzQ2lBZ0lDQWlhWE56ZFdWa1gzUnZJam9nSWtGamJXVWdRMjh1SWl3S0lDQWdJQ0pwYzNOMVpXUmZZWFFpT2lBaU1qQXlNaTB4TWkwek1WUXhNam93TURvd01Gb2lMQW9nSUNBZ0ltVjRjR2x5WlhOZllYUWlPaUFpTWpBeU15MHhNaTB6TVZReE1qb3dNRG93TUZvaUNuMEsiLCJzaWduYXR1cmUiOiJMOWxTT3dkL2VjMmlpZVlYYUFSRENlUzhtaE5INS85c1M0VHQvNkJVMHJmQXMraTRLYVJRV1p5eG9Id203eC8vb2VReXd4cmN1VGpQUXFvemFHbHJEdz09In0K"
)

func Test_getLicense(t *testing.T) {
t.Run("ok", func(t *testing.T) {
publicKey, err := parsePublicKey(publicKeyPEM)
require.NoError(t, err)
contents, err := getLicense(validExpiredTestLicense, publicKey)
require.NoError(t, err)
require.NotEmpty(t, contents)

require.Equal(t, "f82fbafd-6b75-4b3e-af43-7ad405c8d48b", contents.Id)
require.Equal(t, "v1", contents.Version)
require.Equal(t, time.Date(2023, 12, 31, 12, 0, 0, 0, time.UTC), contents.ExpiresAt)
require.Equal(t, time.Date(2022, 12, 31, 12, 0, 0, 0, time.UTC), contents.IssuedAt)
require.Equal(t, "Acme Co.", contents.IssuedTo)
require.False(t, contents.IsValid())
})
}

func Test_NewFromEnv(t *testing.T) {
t.Run("present", func(t *testing.T) {
viper.Set(cloudEnabledEvKey, true)
viper.Set(cloudLicenseEvKey, validExpiredTestLicense)
eelicense, err := NewFromEnv()
require.NoError(t, err)
require.NotNil(t, eelicense)

require.False(t, eelicense.IsValid())
})
t.Run("empty", func(t *testing.T) {
viper.Set(cloudLicenseEvKey, "")

viper.Set(cloudLicenseEvKey, validExpiredTestLicense)
eelicense, err := NewFromEnv()
require.NoError(t, err)
require.NotNil(t, eelicense)

require.False(t, eelicense.IsValid())
})
}
3 changes: 3 additions & 0 deletions internal/ee/cloud-license/neosync_cloud_pub.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAovxmeEF1/FShZgPv7p3J1A1FAdmbhFvFp8P0MbrNIEc=
-----END PUBLIC KEY-----
22 changes: 0 additions & 22 deletions internal/ee/license/cloud.go

This file was deleted.

4 changes: 2 additions & 2 deletions internal/ee/license/license.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ func getLicenseFromEnv() (*licenseContents, bool, error) {
}
pk, err := parsePublicKey(publicKeyPEM)
if err != nil {
return nil, false, err
return nil, false, fmt.Errorf("unable to parse ee public key: %w", err)
}
contents, err := getLicense(input, pk)
if err != nil {
return nil, false, err
return nil, false, fmt.Errorf("failed to parse provided ee license: %w", err)
}
return contents, true, nil
}
Expand Down
Loading