Skip to content

Commit

Permalink
AWS watermark backend
Browse files Browse the repository at this point in the history
  • Loading branch information
e-asphyx committed Aug 14, 2024
1 parent af857a2 commit 6914188
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 12 deletions.
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@ require (
cloud.google.com/go/iam v1.1.5 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.4 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
Expand All @@ -61,6 +65,7 @@ require (
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOm
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.10 h1:orAIBscNu5aIjDOnKIrjO+IUFPMLKj3Lp0bPf4chiPc=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.10/go.mod h1:GNjJ8daGhv10hmQYCnmkV8HuY6xXOXV4vzBssSjEIlU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
Expand All @@ -25,8 +27,14 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iE
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.4 h1:utG3S4T+X7nONPIpRoi1tVcQdAdJxntiVS2yolPJyXc=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.4/go.mod h1:q9vzW3Xr1KEXa8n4waHiFt1PrppNDlMymlYP+xpsFbY=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.3 h1:r27/FnxLPixKBRIlslsvhqscBuMK8uysCYG9Kfgm098=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.3/go.mod h1:jqOFyN+QSWSoQC+ppyc4weiO8iNQXbzRbxDjQ1ayYd4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.16 h1:lhAX5f7KpgwyieXjbDnRTjPEUI0l3emSRyxXj1PXP8w=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.16/go.mod h1:AblAlCwvi7Q/SFowvckgN+8M3uFPlopSYeLlbNDArhA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 h1:UPTdlTOwWUX49fVi7cymEN6hDqCwe3LNv1vi7TXUutk=
Expand Down Expand Up @@ -167,6 +175,9 @@ github.com/hashicorp/vault/api/auth/approle v0.5.0 h1:a1TK6VGwYqSAfkmX4y4dJ4WBxM
github.com/hashicorp/vault/api/auth/approle v0.5.0/go.mod h1:CHOQIA1AZACfjTzHggmyfiOZ+xCSKNRFqe48FTCzH0k=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/karalabe/hid v1.0.0 h1:+/CIMNXhSU/zIJgnIvBD2nKHxS/bnRHhhs9xBryLpPo=
github.com/karalabe/hid v1.0.0/go.mod h1:Vr51f8rUOLYrfrWDFlV12GGQgM5AT8sVh+2fY4MPeu8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down Expand Up @@ -352,6 +363,7 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
2 changes: 1 addition & 1 deletion pkg/signatory/signatory.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ func (s *Signatory) Sign(ctx context.Context, req *SignRequest) (crypt.Signature
l.WithField(logRaw, hex.EncodeToString(req.Message)).Log(level, "About to sign raw bytes")
digest := crypt.DigestFunc(req.Message)
signFunc := func(ctx context.Context, message []byte, key vault.StoredKey) (crypt.Signature, error) {
if err = s.config.Watermark.IsSafeToSign(req.PublicKeyHash, msg, &digest); err != nil {
if err = s.config.Watermark.IsSafeToSign(ctx, req.PublicKeyHash, msg, &digest); err != nil {
err = errors.Wrap(err, http.StatusConflict)
l.Error(err)
return nil, err
Expand Down
190 changes: 190 additions & 0 deletions pkg/signatory/watermark/aws.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package watermark

import (
"context"
"errors"
"fmt"
"strings"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/aws/smithy-go"
tz "github.com/ecadlabs/gotez/v2"
"github.com/ecadlabs/gotez/v2/crypt"
"github.com/ecadlabs/gotez/v2/protocol"
"github.com/ecadlabs/signatory/pkg/signatory/request"
awskms "github.com/ecadlabs/signatory/pkg/vault/aws"
log "github.com/sirupsen/logrus"
)

const (
readCapacityUnits = 5
writeCapacityUnits = 5
defaultTable = "watermark"
)

type Config struct {
awskms.Config
Table string `yaml:"table"`
}

func (c *Config) table() string {
if c.Table != "" {
return c.Table
}
return defaultTable
}

type AWS struct {
cfg Config
client *dynamodb.Client
}

func NewAWSWatermark(ctx context.Context, config *Config) (*AWS, error) {
cfg, err := awskms.NewConfig(ctx, &config.Config)
if err != nil {
return nil, err
}

client := dynamodb.NewFromConfig(cfg)
a := AWS{
client: client,
cfg: *config,
}
if err := a.maybeCreateTable(ctx); err != nil {
return nil, fmt.Errorf("(AWSWatermark) NewAWSWatermark: %w", err)
}
return &a, nil
}

func (a *AWS) maybeCreateTable(ctx context.Context) error {
_, err := a.client.CreateTable(ctx, &dynamodb.CreateTableInput{
AttributeDefinitions: []types.AttributeDefinition{
{
AttributeName: aws.String("idx"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("request"),
AttributeType: types.ScalarAttributeTypeS,
},
},
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("idx"),
KeyType: types.KeyTypeHash,
},
{
AttributeName: aws.String("request"),
KeyType: types.KeyTypeRange,
},
},
ProvisionedThroughput: &types.ProvisionedThroughput{
ReadCapacityUnits: aws.Int64(readCapacityUnits),
WriteCapacityUnits: aws.Int64(writeCapacityUnits),
},
TableName: aws.String(a.cfg.table()),
})
if err != nil {
var serr smithy.APIError
if errors.As(err, &serr) && serr.ErrorCode() == "ResourceInUseException" {
return nil
}
return err
}
log.WithField("table", a.cfg.table()).Info("table created")
waiter := dynamodb.NewTableExistsWaiter(a.client)
return waiter.Wait(context.TODO(), &dynamodb.DescribeTableInput{
TableName: aws.String(a.cfg.table()),
}, time.Minute*5) // give excess time
}

type watermark struct {
Idx string `dynamodbav:"idx"`
Request string `dynamodbav:"request"`
Level int32 `dynamodbav:"lvl"`
Round int32 `dynamodbav:"round"`
Digest *tz.BlockPayloadHash `dynamodbav:"digest"`
}

func (w *watermark) key() map[string]types.AttributeValue {
return map[string]types.AttributeValue{
"idx": &types.AttributeValueMemberS{Value: w.Idx},
"request": &types.AttributeValueMemberS{Value: w.Request},
}
}

func (w *watermark) watermark() *request.Watermark {
return &request.Watermark{
Level: w.Level,
Round: w.Round,
Hash: tz.Some(*w.Digest),
}
}

func (a *AWS) IsSafeToSign(ctx context.Context, pkh crypt.PublicKeyHash, req protocol.SignRequest, digest *crypt.Digest) error {
m, ok := req.(request.WithWatermark)
if !ok {
// watermark is not required
return nil
}
wm := request.NewWatermark(m, digest)
prev := watermark{
Idx: strings.Join([]string{m.GetChainID().String(), pkh.String()}, "/"),
Request: req.SignRequestKind(),
}
for {
response, err := a.client.GetItem(ctx, &dynamodb.GetItemInput{
Key: prev.key(),
TableName: aws.String(a.cfg.table()),
})
if err != nil {
return fmt.Errorf("(AWSWatermark) IsSafeToSign: %w", err)
}

update := watermark{
Idx: prev.Idx,
Request: prev.Request,
Level: wm.Level,
Round: wm.Round,
Digest: wm.Hash.UnwrapPtr(),
}
item, err := attributevalue.MarshalMap(&update)
if err != nil {
return fmt.Errorf("(AWSWatermark) IsSafeToSign: %w", err)
}
input := dynamodb.PutItemInput{
TableName: aws.String(a.cfg.table()),
Item: item,
}

if response.Item != nil {
if err := attributevalue.UnmarshalMap(response.Item, &prev); err != nil {
return fmt.Errorf("(AWSWatermark) IsSafeToSign: %w", err)
}
if !wm.Validate(prev.watermark()) {
return ErrWatermark
}
input.ConditionExpression = aws.String("lvl = :lvl AND round = :round AND digest = :digest")
input.ExpressionAttributeValues = map[string]types.AttributeValue{
":lvl": response.Item["lvl"],
":round": response.Item["round"],
":digest": response.Item["digest"],
}
} else {
input.ConditionExpression = aws.String("attribute_not_exists(idx)")
}

_, err = a.client.PutItem((ctx), &input)
var serr smithy.APIError
if err == nil {
return nil
} else if !errors.As(err, &serr) || serr.ErrorCode() != "ConditionalCheckFailedException" {
return fmt.Errorf("(AWSWatermark) IsSafeToSign: %w", err)
}
// retry
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package watermark

import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -147,7 +148,7 @@ func writeWatermarkData(baseDir string, data delegateMap, chain *tz.ChainID) err
return w.Flush()
}

func (f *File) IsSafeToSign(pkh crypt.PublicKeyHash, req protocol.SignRequest, digest *crypt.Digest) error {
func (f *File) IsSafeToSign(ctx context.Context, pkh crypt.PublicKeyHash, req protocol.SignRequest, digest *crypt.Digest) error {
m, ok := req.(request.WithWatermark)
if !ok {
// watermark is not required
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package watermark

import (
"context"
"sync"

tz "github.com/ecadlabs/gotez/v2"
Expand All @@ -16,7 +17,7 @@ type InMemory struct {
}

// IsSafeToSign return true if this msgID is safe to sign
func (w *InMemory) IsSafeToSign(pkh crypt.PublicKeyHash, req protocol.SignRequest, digest *crypt.Digest) error {
func (w *InMemory) IsSafeToSign(ctx context.Context, pkh crypt.PublicKeyHash, req protocol.SignRequest, digest *crypt.Digest) error {
w.mtx.Lock()
defer w.mtx.Unlock()
return w.isSafeToSignUnlocked(pkh, req, digest)
Expand Down
File renamed without changes.
33 changes: 31 additions & 2 deletions pkg/signatory/watermark/watermark.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,50 @@
package watermark

import (
"context"
"fmt"

"github.com/ecadlabs/gotez/v2/crypt"
"github.com/ecadlabs/gotez/v2/protocol"
"gopkg.in/yaml.v3"
)

// Watermark tests level against stored high watermark
type Watermark interface {
IsSafeToSign(pkh crypt.PublicKeyHash, req protocol.SignRequest, digest *crypt.Digest) error
IsSafeToSign(ctx context.Context, pkh crypt.PublicKeyHash, req protocol.SignRequest, digest *crypt.Digest) error
}

// Ignore watermark that do not validation and return true
type Ignore struct{}

// IsSafeToSign always return true
func (w Ignore) IsSafeToSign(crypt.PublicKeyHash, protocol.SignRequest, *crypt.Digest) error {
func (w Ignore) IsSafeToSign(context.Context, crypt.PublicKeyHash, protocol.SignRequest, *crypt.Digest) error {
return nil
}

var _ Watermark = (*Ignore)(nil)

type Factory interface {
New(ctx context.Context, name string, conf *yaml.Node) (Watermark, error)
}

type newWMBackendFunc func(ctx context.Context, conf *yaml.Node) (Watermark, error)

type registry map[string]newWMBackendFunc

func (r registry) New(ctx context.Context, name string, conf *yaml.Node) (Watermark, error) {
if newFunc, ok := r[name]; ok {
return newFunc(ctx, conf)
}
return nil, fmt.Errorf("unknown watermark backend: %s", name)
}

var wmRegistry = make(registry)

func RegisterWatermark(name string, newFunc newWMBackendFunc) {
wmRegistry[name] = newFunc
}

func Registry() Factory {
return wmRegistry
}
5 changes: 3 additions & 2 deletions pkg/signatory/watermark/watermark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package watermark

import (
"context"
"fmt"
"os"
"testing"
Expand Down Expand Up @@ -103,7 +104,7 @@ func TestWatermark(t *testing.T) {
var wm InMemory
for i, c := range cases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
err := wm.IsSafeToSign(c.pkh, c.req, &c.reqDigest)
err := wm.IsSafeToSign(context.Background(), c.pkh, c.req, &c.reqDigest)
if c.expectErr {
assert.Error(t, err)
} else {
Expand All @@ -120,7 +121,7 @@ func TestWatermark(t *testing.T) {
require.NoError(t, err)
for i, c := range cases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
err := wm.IsSafeToSign(c.pkh, c.req, &c.reqDigest)
err := wm.IsSafeToSign(context.Background(), c.pkh, c.req, &c.reqDigest)
if c.expectErr {
assert.Error(t, err)
} else {
Expand Down
Loading

0 comments on commit 6914188

Please sign in to comment.