Skip to content
Open
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
2 changes: 2 additions & 0 deletions .drone.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ local build(arch) = {
SYNCLOUD_TOKEN: { from_secret: "uat_token" },
AWS_ACCESS_KEY_ID: { from_secret: "AWS_ACCESS_KEY_ID" },
AWS_SECRET_ACCESS_KEY: { from_secret: "AWS_SECRET_ACCESS_KEY" },
SIGNING_KEY_NEW_BASE64: { from_secret: "signing_key_new_base64" },
},
commands: [
"./ci/deploy-prepare.sh uat",
Expand All @@ -208,6 +209,7 @@ local build(arch) = {
SYNCLOUD_TOKEN: { from_secret: "prod_token" },
AWS_ACCESS_KEY_ID: { from_secret: "AWS_ACCESS_KEY_ID" },
AWS_SECRET_ACCESS_KEY: { from_secret: "AWS_SECRET_ACCESS_KEY" },
SIGNING_KEY_NEW_BASE64: { from_secret: "signing_key_new_base64" },
},
commands: [
"./ci/deploy-prepare.sh prod",
Expand Down
5 changes: 5 additions & 0 deletions ci/deploy-prepare.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ set +x
sed -i "s|@aws_secret_access_key@|${AWS_SECRET_ACCESS_KEY}|g" "$STAGE/secret.yaml"
set -x

# optional: only consumed once signing_key_active is switched to "new"; empty otherwise
set +x
sed -i "s|@signing_key_new_base64@|${SIGNING_KEY_NEW_BASE64}|g" "$STAGE/secret.yaml"
set -x

$SSH $REMOTE "sudo -n rm -rf /tmp/syncloud-store && mkdir -p /tmp/syncloud-store/config/${ENV}"
$SCP deploy "${REMOTE}:/tmp/syncloud-store/"
$SCP "$STAGE/." "${REMOTE}:/tmp/syncloud-store/config/${ENV}/"
11 changes: 10 additions & 1 deletion cmd/store/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/base64"
"fmt"
"io/fs"
"net/url"
Expand Down Expand Up @@ -70,7 +71,15 @@ func start(listenAddress, configPath, metricsAddr string) error {
return err
}
cache := storage.New(client, mp, config.BaseUrl, logger)
signer := crypto.NewSigner(logger)
newSigningKey := ""
if config.SigningKeyActive == "new" {
decoded, err := base64.StdEncoding.DecodeString(config.SigningKeyNewBase64)
if err != nil {
return fmt.Errorf("cannot decode signing_key_new_base64: %v", err)
}
newSigningKey = string(decoded)
}
signer := crypto.NewSigner(logger, config.SigningKeyActive, newSigningKey)
popularity := storage.NewPopularity()
snapdMetrics := api.NewSnapdMetrics()
ui := api.NewWeb(webFS, cache, popularity)
Expand Down
2 changes: 2 additions & 0 deletions config/prod/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ aws_access_key_id: "@aws_access_key_id@"
aws_secret_access_key: "@aws_secret_access_key@"
aws_s3_endpoint: https://s3.us-west-2.amazonaws.com
aws_region: us-west-2
signing_key_active: old
signing_key_new_base64: "@signing_key_new_base64@"
2 changes: 2 additions & 0 deletions config/test/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ aws_access_key_id: "@aws_access_key_id@"
aws_secret_access_key: "@aws_secret_access_key@"
aws_s3_endpoint: http://apps.s3
aws_region: garage
signing_key_active: old
signing_key_new_base64: "@signing_key_new_base64@"
2 changes: 2 additions & 0 deletions config/uat/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ aws_access_key_id: "@aws_access_key_id@"
aws_secret_access_key: "@aws_secret_access_key@"
aws_s3_endpoint: https://s3.us-west-2.amazonaws.com
aws_region: us-west-2
signing_key_active: old
signing_key_new_base64: "@signing_key_new_base64@"
129 changes: 76 additions & 53 deletions crypto/signer.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package crypto

import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strconv"

"github.com/syncloud/store/model"
"go.uber.org/zap"
"strconv"
"time"
)

const (
Expand Down Expand Up @@ -74,81 +75,103 @@ type PrivateKeySigner struct {
logger *zap.Logger
}

func NewSigner(logger *zap.Logger) *PrivateKeySigner {
privateKey, _ := ReadPrivateKey(syncloudPrivKey)
func NewSigner(logger *zap.Logger, activeKey string, newKeyArmored string) *PrivateKeySigner {
keyText := syncloudPrivKey
if activeKey == "new" {
if newKeyArmored == "" {
logger.Warn("signing_key_active is 'new' but no new key was provided, falling back to the old key")
} else {
logger.Info("signing assertions with the new signing key")
keyText = newKeyArmored
}
}
privateKey, _ := ReadPrivateKey(keyText)
return &PrivateKeySigner{
privateKey: privateKey,
logger: logger,
}
}

const assertionTimestamp = "2016-04-01T00:00:00Z"

type assertionHeader struct {
key string
value string
}

func (s *PrivateKeySigner) SnapRevision(key, revision string) (string, error) {
var snapRevision model.SnapRevision
err := json.Unmarshal([]byte(revision), &snapRevision)
if err != nil {
return "", err
}
headers := "" +
"snap-revision: " + snapRevision.Revision + "\n" +
"snap-id: " + snapRevision.Id + "\n" +
"snap-size: " + snapRevision.Size + "\n" +
"snap-sha3-384: " + snapRevision.Sha384 + "\n"

return s.sign("snap-revision", key, headers, "")
return s.assemble("snap-revision",
[]assertionHeader{{"snap-sha3-384", key}},
map[string]string{
"snap-id": snapRevision.Id,
"snap-revision": snapRevision.Revision,
"snap-size": snapRevision.Size,
"developer-id": "syncloud",
"timestamp": assertionTimestamp,
}, "")
}

func (s *PrivateKeySigner) SnapDeclaration(series, snapId string) (string, error) {
name := model.SnapId(snapId).Name()
headers := "" +
"series: " + series + "\n" +
"snap-id: " + snapId + "\n" +
"snap-name: " + name + "\n"

return s.sign("snap-declaration", fmt.Sprintf("%s/%s", series, snapId), headers, "")

return s.assemble("snap-declaration",
[]assertionHeader{{"series", series}, {"snap-id", snapId}},
map[string]string{
"snap-name": model.SnapId(snapId).Name(),
"publisher-id": "syncloud",
"timestamp": assertionTimestamp,
}, "")
}

func (s *PrivateKeySigner) AccountKey(key string) (string, error) {
publicKeyEnc, err := s.privateKey.PublicKey().EncodeKey()
if err != nil {
return "", err
}

return s.sign("account-key", key, "", string(publicKeyEnc))
return s.assemble("account-key",
[]assertionHeader{{"public-key-sha3-384", s.privateKey.PublicKey().ID()}},
map[string]string{
"account-id": "syncloud",
"name": "root",
"since": assertionTimestamp,
}, string(publicKeyEnc))
}

func (s *PrivateKeySigner) sign(assertType string, primaryKey string, headers string, body string) (string, error) {
publicKeyId := s.privateKey.PublicKey().ID()

s.logger.Info("public key", zap.String("id", publicKeyId))

content := "type: " + assertType + "\n" +
"authority-id: syncloud\n" +
"primary-key: " + primaryKey + "\n" +
"publisher-id: syncloud\n" +
"developer-id: syncloud\n" +
"account-id: syncloud\n" +
// "display-name: syncloud\n" +
"revision: 1\n" +
"sign-key-sha3-384: " + publicKeyId + "\n" +
"sha3-384: " + publicKeyId + "\n" +
"public-key-sha3-384: " + publicKeyId + "\n" +
"timestamp: " + time.Now().Format(time.RFC3339) + "\n" +
"since: " + time.Now().Format(time.RFC3339) + "\n" +
headers +
"validation: certified\n" +
"body-length: " + strconv.Itoa(len(body)) + "\n\n" +
body +
"\n\n"

fmt.Println("content:")
fmt.Println(content)
signature, err := s.privateKey.SignContent([]byte(content))
// assemble builds an assertion in snapd's canonical serialization and
// self-signs it, so snapd verifies it with real signature checking. The header
// order must match asserts.assembleAndSign exactly: type, authority-id, primary
// keys, remaining headers in lexicographic order, body-length, then
// sign-key-sha3-384, followed by the body.
func (s *PrivateKeySigner) assemble(assertType string, primary []assertionHeader, other map[string]string, body string) (string, error) {
keyID := s.privateKey.PublicKey().ID()
var buf bytes.Buffer
buf.WriteString("type: " + assertType)
buf.WriteString("\nauthority-id: syncloud")
for _, h := range primary {
buf.WriteString("\n" + h.key + ": " + h.value)
}
keys := make([]string, 0, len(other))
for k := range other {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
buf.WriteString("\n" + k + ": " + other[k])
}
if body != "" {
buf.WriteString("\nbody-length: " + strconv.Itoa(len(body)))
}
buf.WriteString("\nsign-key-sha3-384: " + keyID)
if body != "" {
buf.WriteString("\n\n" + body)
}
content := buf.Bytes()
signature, err := s.privateKey.SignContent(content)
if err != nil {
return "", err
}
fmt.Println("signature:")
fmt.Println(string(signature))
assertionText := content + string(signature) + "\n"
return assertionText, nil
return string(content) + "\n\n" + string(signature) + "\n", nil
}
89 changes: 87 additions & 2 deletions crypto/signer_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,100 @@
package crypto

import (
"bytes"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/syncloud/store/log"
"testing"
"golang.org/x/crypto/openpgp/armor"
"golang.org/x/crypto/openpgp/packet"
)

var fixedTs = time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC)

func armorKey(t *testing.T, rsaKey *rsa.PrivateKey) string {
var buf bytes.Buffer
arm, err := armor.Encode(&buf, "PGP PRIVATE KEY BLOCK", nil)
require.NoError(t, err)
require.NoError(t, packet.NewRSAPrivateKey(fixedTs, rsaKey).Serialize(arm))
require.NoError(t, arm.Close())
return buf.String()
}

// honestVerify checks the assertion's signature the way real snapd does
// (raw openpgp), independent of the fork's disabled SignatureCheck. The signed
// content is everything before the final "\n\n"; the signature follows it.
func honestVerify(t *testing.T, assertionText string, pub *rsa.PublicKey) {
idx := strings.LastIndex(assertionText, "\n\n")
require.GreaterOrEqual(t, idx, 0)
content := assertionText[:idx]
sigB64 := strings.ReplaceAll(strings.TrimRight(assertionText[idx+2:], "\n"), "\n", "")
raw, err := base64.StdEncoding.DecodeString(sigB64)
require.NoError(t, err)
require.Equal(t, byte(0x01), raw[0])
pkt, err := packet.Read(bytes.NewReader(raw[1:]))
require.NoError(t, err)
sig := pkt.(*packet.Signature)
pubPkt := packet.NewRSAPublicKey(fixedTs, pub)
h := sig.Hash.New()
h.Write([]byte(content))
assert.NoError(t, pubPkt.VerifySignature(h, sig))
}

func assertSignerVerifies(t *testing.T, signer *PrivateKeySigner, pub *rsa.PublicKey) {
ak, err := signer.AccountKey("test")
require.NoError(t, err)
honestVerify(t, ak, pub)

sd, err := signer.SnapDeclaration("16", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
require.NoError(t, err)
honestVerify(t, sd, pub)

rev := `{"snap-revision":"7","snap-id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","snap-size":"1024"}`
sr, err := signer.SnapRevision("NX7IJUakITdsfLFyqWHkdS-YTp77S-v2-vckEZ1cVBwElYYld8_34m6Ni9JZ1aQL", rev)
require.NoError(t, err)
honestVerify(t, sr, pub)
}

func TestSigner_AssertionsVerifyHonestly_OldKey(t *testing.T) {
_, oldRsa := ReadPrivateKey(syncloudPrivKey)
assertSignerVerifies(t, NewSigner(log.Default(), "old", ""), &oldRsa.PublicKey)
}

func TestSigner_AssertionsVerifyHonestly_NewKey(t *testing.T) {
newRsa, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
assertSignerVerifies(t, NewSigner(log.Default(), "new", armorKey(t, newRsa)), &newRsa.PublicKey)
}

func TestPrivateKeySigner_AccountKey(t *testing.T) {
signer := NewSigner(log.Default())
signer := NewSigner(log.Default(), "old", "")
content, err := signer.AccountKey("test")
assert.NoError(t, err)
assert.Contains(t, content, "public-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR")

}

func TestPrivateKeySigner_NewKeySelected(t *testing.T) {
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
assert.NoError(t, err)
var buf bytes.Buffer
arm, err := armor.Encode(&buf, "PGP PRIVATE KEY BLOCK", nil)
assert.NoError(t, err)
assert.NoError(t, packet.NewRSAPrivateKey(time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC), rsaKey).Serialize(arm))
assert.NoError(t, arm.Close())

priv := RSAPrivateKey(rsaKey)
want := priv.PublicKey().ID()
signer := NewSigner(log.Default(), "new", buf.String())
content, err := signer.AccountKey("test")
assert.NoError(t, err)
assert.Contains(t, content, "public-key-sha3-384: "+want)
assert.NotContains(t, content, "hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR")
}
16 changes: 9 additions & 7 deletions util/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import (
)

type Config struct {
Token string `yaml:"token"`
BaseUrl string `yaml:"base_url"`
Bucket string `yaml:"bucket"`
AwsAccessKeyId string `yaml:"aws_access_key_id"`
AwsSecretAccessKey string `yaml:"aws_secret_access_key"`
AwsS3Endpoint string `yaml:"aws_s3_endpoint"`
AwsRegion string `yaml:"aws_region"`
Token string `yaml:"token"`
BaseUrl string `yaml:"base_url"`
Bucket string `yaml:"bucket"`
AwsAccessKeyId string `yaml:"aws_access_key_id"`
AwsSecretAccessKey string `yaml:"aws_secret_access_key"`
AwsS3Endpoint string `yaml:"aws_s3_endpoint"`
AwsRegion string `yaml:"aws_region"`
SigningKeyActive string `yaml:"signing_key_active"`
SigningKeyNewBase64 string `yaml:"signing_key_new_base64"`
}

func LoadConfig(path string) (Config, error) {
Expand Down