From 25a969e1c38e3e766c35701593ebc1e33f4f75f7 Mon Sep 17 00:00:00 2001 From: Leonhard Oelke Date: Wed, 2 Apr 2025 14:35:36 +0000 Subject: [PATCH 1/2] feat: Add signature caching with memcached Add a memcached option to keys. When enabled, it allows for caching of signatures in case the same file gets signed multiple times. Signed-off-by: Leonhard Oelke --- config/config.go | 1 + doc/relic.yml | 4 ++ internal/signinit/signinit.go | 10 ++++ token/signaturecache/signaturecache.go | 76 ++++++++++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 token/signaturecache/signaturecache.go 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..05a5474 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, 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..a757d59 --- /dev/null +++ b/token/signaturecache/signaturecache.go @@ -0,0 +1,76 @@ +// 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" +) + +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 + signer crypto.Signer + Memcache *memcache.Client +} + +func New(keyConf *config.KeyConfig, 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, 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("%s-%x", c.keyConf.Name(), digest) + + item, err := c.Memcache.Get(cacheKey) + if err == nil && item != nil { + 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), + }) + + return signature, nil +} From 82c17fd9d71bba1898b0e7fb742bf28d81734510 Mon Sep 17 00:00:00 2001 From: Leonhard Oelke Date: Thu, 3 Apr 2025 07:16:15 +0000 Subject: [PATCH 2/2] feat: Add cache info to audit messages When caching for a key is enabled, this will add an additional property to the audit message to show if the signature was fetched from cache. Signed-off-by: Leonhard Oelke --- internal/signinit/signinit.go | 2 +- token/signaturecache/signaturecache.go | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/signinit/signinit.go b/internal/signinit/signinit.go index 05a5474..109b2f3 100644 --- a/internal/signinit/signinit.go +++ b/internal/signinit/signinit.go @@ -89,7 +89,7 @@ func Init(ctx context.Context, mod *signers.Signer, tok token.Token, keyName str // Use signature cache if configured if len(kconf.Memcache) > 0 { - cert.PrivateKey, err = signaturecache.New(kconf, cert.PrivateKey.(crypto.Signer)) + cert.PrivateKey, err = signaturecache.New(kconf, &opts, cert.PrivateKey.(crypto.Signer)) if err != nil { return nil, nil, err } diff --git a/token/signaturecache/signaturecache.go b/token/signaturecache/signaturecache.go index a757d59..f823cdb 100644 --- a/token/signaturecache/signaturecache.go +++ b/token/signaturecache/signaturecache.go @@ -21,6 +21,7 @@ import ( "github.com/bradfitz/gomemcache/memcache" "github.com/sassoftware/relic/v8/config" + "github.com/sassoftware/relic/v8/signers" ) const ( @@ -35,18 +36,19 @@ type SignatureCache interface { type signatureCache struct { keyConf *config.KeyConfig + opts *signers.SignOpts signer crypto.Signer Memcache *memcache.Client } -func New(keyConf *config.KeyConfig, signer crypto.Signer) (SignatureCache, error) { +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, signer, mc}, nil + return &signatureCache{keyConf, opts, signer, mc}, nil } func (c *signatureCache) Public() crypto.PublicKey { @@ -54,10 +56,11 @@ func (c *signatureCache) Public() crypto.PublicKey { } func (c *signatureCache) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { - cacheKey := fmt.Sprintf("%s-%x", c.keyConf.Name(), digest) + 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 } @@ -72,5 +75,6 @@ func (c *signatureCache) Sign(rand io.Reader, digest []byte, opts crypto.SignerO Expiration: int32(memcacheExpiry / time.Second), }) + c.opts.Audit.Attributes["sig.cache"] = "miss" return signature, nil }