Skip to content

Commit

Permalink
NEOS-1612: adds separate public key pair for neosync cloud checks (#2992
Browse files Browse the repository at this point in the history
)
  • Loading branch information
nickzelei authored Nov 26, 2024
1 parent 73b9174 commit 78afd79
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 46 deletions.
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

0 comments on commit 78afd79

Please sign in to comment.