-
Notifications
You must be signed in to change notification settings - Fork 125
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NEOS-1612: adds separate public key pair for neosync cloud checks (#2992
- Loading branch information
Showing
8 changed files
with
243 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,3 +38,4 @@ venv2/ | |
python/dist/ | ||
|
||
neosync_ee_ca.key | ||
neosync_cloud_ca.key |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
-----BEGIN PUBLIC KEY----- | ||
MCowBQYDK2VwAyEAovxmeEF1/FShZgPv7p3J1A1FAdmbhFvFp8P0MbrNIEc= | ||
-----END PUBLIC KEY----- |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.