diff --git a/config/config.go b/config/config.go index a6f3d63..24c8b87 100644 --- a/config/config.go +++ b/config/config.go @@ -70,6 +70,7 @@ type KeyConfig struct { Timestamp bool // If true, attach a timestamped countersignature when possible Timestamper string // If set, use the named timestamper to countersign Hide bool // If true, then omit this key from 'remote list-keys' + Memcache []string // host:port of memcached to use for caching signatures name string token *TokenConfig diff --git a/doc/relic.yml b/doc/relic.yml index 9323afb..7fc209c 100644 --- a/doc/relic.yml +++ b/doc/relic.yml @@ -96,6 +96,10 @@ keys: # see `namedurls` below. Implies "timestamp: true". #timestamper: apple + # Optional memcache servers for memoizing signing requests + #memcache: + # - 127.0.0.1:11211 + # Clients with any of these roles can utilize this key roles: ["somegroup"] diff --git a/internal/signinit/signinit.go b/internal/signinit/signinit.go index bb35c3a..109b2f3 100644 --- a/internal/signinit/signinit.go +++ b/internal/signinit/signinit.go @@ -29,6 +29,7 @@ import ( "github.com/sassoftware/relic/v8/signers" "github.com/sassoftware/relic/v8/signers/sigerrors" "github.com/sassoftware/relic/v8/token" + "github.com/sassoftware/relic/v8/token/signaturecache" ) // InitKey loads the cert chain for a key @@ -85,6 +86,15 @@ func Init(ctx context.Context, mod *signers.Signer, tok token.Token, keyName str Audit: auditInfo, Flags: flags, } + + // Use signature cache if configured + if len(kconf.Memcache) > 0 { + cert.PrivateKey, err = signaturecache.New(kconf, &opts, cert.PrivateKey.(crypto.Signer)) + if err != nil { + return nil, nil, err + } + } + opts = opts.WithContext(ctx) return cert, &opts, nil } diff --git a/token/signaturecache/signaturecache.go b/token/signaturecache/signaturecache.go new file mode 100644 index 0000000..f823cdb --- /dev/null +++ b/token/signaturecache/signaturecache.go @@ -0,0 +1,80 @@ +// Copyright © Leonhard Oelke +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package signaturecache + +import ( + "crypto" + "fmt" + "io" + "time" + + "github.com/bradfitz/gomemcache/memcache" + "github.com/sassoftware/relic/v8/config" + "github.com/sassoftware/relic/v8/signers" +) + +const ( + // Settings for now are the same as the timestamp cache settings + memcacheTimeout = 1 * time.Second + memcacheExpiry = 7 * 24 * time.Hour +) + +type SignatureCache interface { + crypto.Signer +} + +type signatureCache struct { + keyConf *config.KeyConfig + opts *signers.SignOpts + signer crypto.Signer + Memcache *memcache.Client +} + +func New(keyConf *config.KeyConfig, opts *signers.SignOpts, signer crypto.Signer) (SignatureCache, error) { + selector := new(memcache.ServerList) + if err := selector.SetServers(keyConf.Memcache...); err != nil { + return nil, fmt.Errorf("parsing memcache servers: %w", err) + } + mc := memcache.NewFromSelector(selector) + mc.Timeout = memcacheTimeout + return &signatureCache{keyConf, opts, signer, mc}, nil +} + +func (c *signatureCache) Public() crypto.PublicKey { + return c.signer.Public() +} + +func (c *signatureCache) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + cacheKey := fmt.Sprintf("sig-%s-%x", c.keyConf.Name(), digest) + + item, err := c.Memcache.Get(cacheKey) + if err == nil && item != nil { + c.opts.Audit.Attributes["sig.cache"] = "hit" + return item.Value, nil + } + + signature, err := c.signer.Sign(rand, digest, opts) + if err != nil { + return nil, err + } + + _ = c.Memcache.Set(&memcache.Item{ + Key: cacheKey, + Value: signature, + Expiration: int32(memcacheExpiry / time.Second), + }) + + c.opts.Audit.Attributes["sig.cache"] = "miss" + return signature, nil +}