api: implement fernet encryption of pagination tokens

This commit is contained in:
Jimmy Zelinskie 2016-02-05 16:22:17 -05:00
parent b8c534cd0d
commit d19a4348df
16 changed files with 520 additions and 23 deletions

4
Godeps/Godeps.json generated
View File

@ -30,6 +30,10 @@
"ImportPath": "github.com/davecgh/go-spew/spew",
"Rev": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d"
},
{
"ImportPath": "github.com/fernet/fernet-go",
"Rev": "1b2437bc582b3cfbb341ee5a29f8ef5b42912ff2"
},
{
"ImportPath": "github.com/go-sql-driver/mysql",
"Comment": "v1.2-125-gd512f20",

View File

@ -23,6 +23,7 @@ import (
"github.com/julienschmidt/httprouter"
"github.com/prometheus/client_golang/prometheus"
"github.com/coreos/clair/config"
"github.com/coreos/clair/database"
"github.com/coreos/clair/utils"
)
@ -58,5 +59,6 @@ func HTTPHandler(handler Handler, ctx *RouteContext) httprouter.Handle {
}
type RouteContext struct {
Store database.Datastore
Store database.Datastore
Config *config.APIConfig
}

View File

@ -15,13 +15,20 @@
package v1
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/coreos/clair/database"
"github.com/coreos/clair/utils/types"
"github.com/coreos/pkg/capnslog"
"github.com/fernet/fernet-go"
)
var log = capnslog.NewPackageLogger("github.com/coreos/clair", "v1")
type Error struct {
Message string `json:"Layer`
}
@ -187,10 +194,9 @@ type Notification struct {
NextPage string `json:"NextPage,omitempty"`
Old *VulnerabilityWithLayers `json:"Old,omitempty"`
New *VulnerabilityWithLayers `json:"New,omitempty"`
Changed []string `json:"Changed,omitempty"`
}
func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotification, limit int, page, nextPage database.VulnerabilityNotificationPageNumber) Notification {
func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotification, limit int, page, nextPage database.VulnerabilityNotificationPageNumber, key string) Notification {
var oldVuln *VulnerabilityWithLayers
if dbNotification.OldVulnerability != nil {
v := VulnerabilityWithLayersFromDatabaseModel(*dbNotification.OldVulnerability)
@ -205,7 +211,7 @@ func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotifica
var nextPageStr string
if nextPage != database.NoVulnerabilityNotificationPage {
nextPageStr = DBPageNumberToString(nextPage)
nextPageStr = pageNumberToToken(nextPage, key)
}
var created, notified, deleted string
@ -227,7 +233,7 @@ func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotifica
Notified: notified,
Deleted: deleted,
Limit: limit,
Page: DBPageNumberToString(page),
Page: pageNumberToToken(page, key),
NextPage: nextPageStr,
Old: oldVuln,
New: newVuln,
@ -279,14 +285,30 @@ type FeatureEnvelope struct {
Error *Error `json:"Error,omitempty"`
}
func pageStringToDBPageNumber(pageStr string) (database.VulnerabilityNotificationPageNumber, error) {
// TODO(jzelinskie): turn pagination into an encrypted token
var old, new int
_, err := fmt.Sscanf(pageStr, "%d-%d", &old, &new)
return database.VulnerabilityNotificationPageNumber{old, new}, err
func tokenToPageNumber(token, key string) (database.VulnerabilityNotificationPageNumber, error) {
k, _ := fernet.DecodeKey(key)
msg := fernet.VerifyAndDecrypt([]byte(token), time.Hour, []*fernet.Key{k})
if msg == nil {
return database.VulnerabilityNotificationPageNumber{}, errors.New("invalid or expired pagination token")
}
page := database.VulnerabilityNotificationPageNumber{}
err := json.NewDecoder(bytes.NewBuffer(msg)).Decode(&page)
return page, err
}
func DBPageNumberToString(page database.VulnerabilityNotificationPageNumber) string {
// TODO(jzelinskie): turn pagination into an encrypted token
return fmt.Sprintf("%d-%d", page.OldVulnerability, page.NewVulnerability)
func pageNumberToToken(page database.VulnerabilityNotificationPageNumber, key string) string {
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(page)
if err != nil {
log.Fatal("failed to encode VulnerabilityNotificationPageNumber")
}
k, _ := fernet.DecodeKey(key)
tokenBytes, err := fernet.EncryptAndSign(buf.Bytes(), k)
if err != nil {
log.Fatal("failed to encrypt VulnerabilityNotificationpageNumber")
}
return string(tokenBytes)
}

View File

@ -321,7 +321,7 @@ func getNotification(w http.ResponseWriter, r *http.Request, p httprouter.Params
page := database.VulnerabilityNotificationFirstPage
pageStrs, pageExists := query["page"]
if pageExists {
page, err = pageStringToDBPageNumber(pageStrs[0])
page, err = tokenToPageNumber(pageStrs[0], ctx.Config.PaginationKey)
if err != nil {
writeResponse(w, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"invalid page format: " + err.Error()}})
return getNotificationRoute, http.StatusBadRequest
@ -337,7 +337,7 @@ func getNotification(w http.ResponseWriter, r *http.Request, p httprouter.Params
return getNotificationRoute, http.StatusInternalServerError
}
notification := NotificationFromDatabaseModel(dbNotification, limit, page, nextPage)
notification := NotificationFromDatabaseModel(dbNotification, limit, page, nextPage, ctx.Config.PaginationKey)
writeResponse(w, http.StatusOK, NotificationEnvelope{Notification: &notification})
return getNotificationRoute, http.StatusOK

View File

@ -53,9 +53,9 @@ func Boot(config *config.Config) {
// Start API
st.Begin()
go api.Run(config.API, &context.RouteContext{db}, st)
go api.Run(config.API, &context.RouteContext{db, config.API}, st)
st.Begin()
go api.RunHealth(config.API, &context.RouteContext{db}, st)
go api.RunHealth(config.API, &context.RouteContext{db, config.API}, st)
// Start updater
st.Begin()

View File

@ -4,7 +4,7 @@
database:
# PostgreSQL Connection string.
# Reference: http://www.postgresql.org/docs/9.4/static/libpq-connect.html
source:
source:
# Number of elements to keep in the cache.
cacheSize: 16384
api:
@ -13,6 +13,8 @@ api:
healthport: 6061
# Maximum time that API requests may take before they time-out with a HTTP 503 error.
timeout: 900s
# 32-bit key used to encrypt pagination tokens
paginationKey: "2E9IgrgWLNb4gjuU0WbiBIudLH8xolz_qxFn--vxJP8="
# Paths to certificates to secure the main API with TLS and client certificate auth.
cafile:
keyfile:

View File

@ -19,6 +19,7 @@ import (
"os"
"time"
"github.com/fernet/fernet-go"
"gopkg.in/yaml.v2"
)
@ -44,9 +45,9 @@ type UpdaterConfig struct {
// NotifierConfig is the configuration for the Notifier service and its registered notifiers.
type NotifierConfig struct {
Attempts int
RenotifyInterval time.Duration
Params map[string]interface{} `yaml:",inline"`
Attempts int
RenotifyInterval time.Duration
Params map[string]interface{} `yaml:",inline"`
}
// APIConfig is the configuration for the API service.
@ -54,6 +55,7 @@ type APIConfig struct {
Port int
HealthPort int
Timeout time.Duration
PaginationKey string
CertFile, KeyFile, CAFile string
}
@ -71,8 +73,8 @@ var DefaultConfig = Config{
Timeout: 900 * time.Second,
},
Notifier: &NotifierConfig{
Attempts: 5,
RenotifyInterval: 2 * time.Hour,
Attempts: 5,
RenotifyInterval: 2 * time.Hour,
},
}
@ -96,5 +98,22 @@ func Load(path string) (config *Config, err error) {
}
err = yaml.Unmarshal(d, config)
if err != nil {
return
}
if config.API.PaginationKey == "" {
var key fernet.Key
if err = key.Generate(); err != nil {
return
}
config.API.PaginationKey = key.Encode()
} else {
_, err = fernet.DecodeKey(config.API.PaginationKey)
if err != nil {
return
}
}
return
}

20
vendor/github.com/fernet/fernet-go/License generated vendored Normal file
View File

@ -0,0 +1,20 @@
Copyright © 2013 Keith Rarick
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

22
vendor/github.com/fernet/fernet-go/Readme generated vendored Normal file
View File

@ -0,0 +1,22 @@
Fernet takes a user-provided *message* (an arbitrary sequence of
bytes), a *key* (256 bits), and the current time, and produces a
*token*, which contains the message in a form that can't be read
or altered without the key.
This package is compatible with the other implementations at
https://github.com/fernet. They can exchange tokens freely among
each other.
Documentation: http://godoc.org/github.com/fernet/fernet-go
INSTALL
$ go get github.com/fernet/fernet-go
For more information and background, see the Fernet spec at
https://github.com/fernet/spec.
Fernet is distributed under the terms of the MIT license.
See the License file for details.

View File

@ -0,0 +1,19 @@
package main
import (
"fmt"
"log"
"github.com/fernet/fernet-go"
)
func main() {
log.SetFlags(0)
log.SetPrefix("fernet: ")
var key fernet.Key
if err := key.Generate(); err != nil {
log.Fatal(err)
}
fmt.Println(key.Encode())
}

View File

@ -0,0 +1,45 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"github.com/fernet/fernet-go"
)
const Usage = `Usage: fernet-sign ENV
fernet-sign encrypts and signs its input and prints the resulting token.
It uses the key in environment variable ENV.`
func main() {
log.SetFlags(0)
log.SetPrefix("fernet: ")
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, Usage)
os.Exit(2)
}
key, err := fernet.DecodeKey(os.Getenv(os.Args[1]))
if err != nil {
log.Fatalln(err)
}
b, err := ioutil.ReadAll(os.Stdin)
if err != nil {
log.Fatalln(err)
}
t, err := fernet.EncryptAndSign(b, key)
if err != nil {
log.Fatalln(err)
}
_, err = os.Stdout.Write(append(t, '\n'))
if err != nil {
log.Fatalln(err)
}
}

168
vendor/github.com/fernet/fernet-go/fernet.go generated vendored Normal file
View File

@ -0,0 +1,168 @@
// Package fernet takes a user-provided message (an arbitrary
// sequence of bytes), a key (256 bits), and the current time,
// and produces a token, which contains the message in a form
// that can't be read or altered without the key.
//
// For more information and background, see the Fernet spec
// at https://github.com/fernet/spec.
//
// Subdirectories in this package provide command-line tools
// for working with Fernet keys and tokens.
package fernet
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/binary"
"io"
"time"
)
const (
version byte = 0x80
tsOffset = 1
ivOffset = tsOffset + 8
payOffset = ivOffset + aes.BlockSize
overhead = 1 + 8 + aes.BlockSize + sha256.Size // ver + ts + iv + hmac
maxClockSkew = 60 * time.Second
)
var encoding = base64.URLEncoding
// generates a token from msg, writes it into tok, and returns the
// number of bytes generated, which is encodedLen(msg).
// len(tok) must be >= encodedLen(len(msg))
func gen(tok, msg, iv []byte, ts time.Time, k *Key) int {
tok[0] = version
binary.BigEndian.PutUint64(tok[tsOffset:], uint64(ts.Unix()))
copy(tok[ivOffset:], iv)
p := tok[payOffset:]
n := pad(p, msg, aes.BlockSize)
bc, _ := aes.NewCipher(k.cryptBytes())
cipher.NewCBCEncrypter(bc, iv).CryptBlocks(p[:n], p[:n])
genhmac(p[n:n], tok[:payOffset+n], k.signBytes())
return payOffset + n + sha256.Size
}
// token length for input msg of length n, not including base64
func encodedLen(n int) int {
const k = aes.BlockSize
return n/k*k + k + overhead
}
// max msg length for tok of length n, for binary token (no base64)
// upper bound; not exact
func decodedLen(n int) int {
return n - overhead
}
// if msg is nil, decrypts in place and returns a slice of tok.
func verify(msg, tok []byte, ttl time.Duration, now time.Time, k *Key) []byte {
if len(tok) < 1 || tok[0] != version {
return nil
}
ts := time.Unix(int64(binary.BigEndian.Uint64(tok[1:])), 0)
if ttl >= 0 && (now.After(ts.Add(ttl)) || ts.After(now.Add(maxClockSkew))) {
return nil
}
n := len(tok) - sha256.Size
var hmac [sha256.Size]byte
genhmac(hmac[:0], tok[:n], k.signBytes())
if subtle.ConstantTimeCompare(tok[n:], hmac[:]) != 1 {
return nil
}
pay := tok[payOffset : len(tok)-sha256.Size]
if len(pay)%aes.BlockSize != 0 {
return nil
}
if msg != nil {
copy(msg, pay)
pay = msg
}
bc, _ := aes.NewCipher(k.cryptBytes())
iv := tok[9:][:aes.BlockSize]
cipher.NewCBCDecrypter(bc, iv).CryptBlocks(pay, pay)
return unpad(pay)
}
// Pads p to a multiple of k using PKCS #7 standard block padding.
// See http://tools.ietf.org/html/rfc5652#section-6.3.
func pad(q, p []byte, k int) int {
n := len(p)/k*k + k
copy(q, p)
c := byte(n - len(p))
for i := len(p); i < n; i++ {
q[i] = c
}
return n
}
// Removes PKCS #7 standard block padding from p.
// See http://tools.ietf.org/html/rfc5652#section-6.3.
// This function is the inverse of pad.
// If the padding is not well-formed, unpad returns nil.
func unpad(p []byte) []byte {
c := p[len(p)-1]
for i := len(p) - int(c); i < len(p); i++ {
if i < 0 || p[i] != c {
return nil
}
}
return p[:len(p)-int(c)]
}
func b64enc(src []byte) []byte {
dst := make([]byte, encoding.EncodedLen(len(src)))
encoding.Encode(dst, src)
return dst
}
func b64dec(src []byte) []byte {
dst := make([]byte, encoding.DecodedLen(len(src)))
n, err := encoding.Decode(dst, src)
if err != nil {
return nil
}
return dst[:n]
}
func genhmac(q, p, k []byte) {
h := hmac.New(sha256.New, k)
h.Write(p)
h.Sum(q)
}
// EncryptAndSign encrypts and signs msg with key k and returns the resulting
// fernet token. If msg contains text, the text should be encoded
// with UTF-8 to follow fernet convention.
func EncryptAndSign(msg []byte, k *Key) (tok []byte, err error) {
iv := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
b := make([]byte, encodedLen(len(msg)))
n := gen(b, msg, iv, time.Now(), k)
tok = make([]byte, encoding.EncodedLen(n))
encoding.Encode(tok, b[:n])
return tok, nil
}
// VerifyAndDecrypt verifies that tok is a valid fernet token that was signed
// with a key in k at most ttl time ago only if ttl is greater than zero.
// Returns the message contained in tok if tok is valid, otherwise nil.
func VerifyAndDecrypt(tok []byte, ttl time.Duration, k []*Key) (msg []byte) {
b := make([]byte, encoding.DecodedLen(len(tok)))
n, _ := encoding.Decode(b, tok)
for _, k1 := range k {
msg = verify(nil, b[:n], ttl, time.Now(), k1)
if msg != nil {
return msg
}
}
return nil
}

9
vendor/github.com/fernet/fernet-go/generate.json generated vendored Normal file
View File

@ -0,0 +1,9 @@
[
{
"token": "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==",
"now": "1985-10-26T01:20:00-07:00",
"iv": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
"src": "hello",
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
}
]

58
vendor/github.com/fernet/fernet-go/invalid.json generated vendored Normal file
View File

@ -0,0 +1,58 @@
[
{
"desc": "incorrect mac",
"token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykQUFBQUFBQUFBQQ==",
"now": "1985-10-26T01:20:01-07:00",
"ttl_sec": 60,
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
},
{
"desc": "too short",
"token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPA==",
"now": "1985-10-26T01:20:01-07:00",
"ttl_sec": 60,
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
},
{
"desc": "invalid base64",
"token": "%%%%%%%%%%%%%AECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykRtfsH-p1YsUD2Q==",
"now": "1985-10-26T01:20:01-07:00",
"ttl_sec": 60,
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
},
{
"desc": "payload size not multiple of block size",
"token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPOm73QeoCk9uGib28Xe5vz6oxq5nmxbx_v7mrfyudzUm",
"now": "1985-10-26T01:20:01-07:00",
"ttl_sec": 60,
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
},
{
"desc": "payload padding error",
"token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0ODz4LEpdELGQAad7aNEHbf-JkLPIpuiYRLQ3RtXatOYREu2FWke6CnJNYIbkuKNqOhw==",
"now": "1985-10-26T01:20:01-07:00",
"ttl_sec": 60,
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
},
{
"desc": "far-future TS (unacceptable clock skew)",
"token": "gAAAAAAdwStRAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAnja1xKYyhd-Y6mSkTOyTGJmw2Xc2a6kBd-iX9b_qXQcw==",
"now": "1985-10-26T01:20:01-07:00",
"ttl_sec": 60,
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
},
{
"desc": "expired TTL",
"token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykRtfsH-p1YsUD2Q==",
"now": "1985-10-26T01:21:31-07:00",
"ttl_sec": 60,
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
},
{
"desc": "incorrect IV (causes padding error)",
"token": "gAAAAAAdwJ6xBQECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAkLhFLHpGtDBRLRTZeUfWgHSv49TF2AUEZ1TIvcZjK1zQ==",
"now": "1985-10-26T01:20:01-07:00",
"ttl_sec": 60,
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
}
]

91
vendor/github.com/fernet/fernet-go/key.go generated vendored Normal file
View File

@ -0,0 +1,91 @@
package fernet
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"io"
)
var (
errKeyLen = errors.New("fernet: key decodes to wrong size")
errNoKeys = errors.New("fernet: no keys provided")
)
// Key represents a key.
type Key [32]byte
func (k *Key) cryptBytes() []byte {
return k[len(k)/2:]
}
func (k *Key) signBytes() []byte {
return k[:len(k)/2]
}
// Generate initializes k with pseudorandom data from package crypto/rand.
func (k *Key) Generate() error {
_, err := io.ReadFull(rand.Reader, k[:])
return err
}
// Encode returns the URL-safe base64 encoding of k.
func (k *Key) Encode() string {
return encoding.EncodeToString(k[:])
}
// DecodeKey decodes a key from s and returns it. The key can be in
// hexadecimal, standard base64, or URL-safe base64.
func DecodeKey(s string) (*Key, error) {
var b []byte
var err error
if s == "" {
return nil, errors.New("empty key")
}
if len(s) == hex.EncodedLen(len(Key{})) {
b, err = hex.DecodeString(s)
} else {
b, err = base64.StdEncoding.DecodeString(s)
if err != nil {
b, err = base64.URLEncoding.DecodeString(s)
}
}
if err != nil {
return nil, err
}
if len(b) != len(Key{}) {
return nil, errKeyLen
}
k := new(Key)
copy(k[:], b)
return k, nil
}
// DecodeKeys decodes each element of a using DecodeKey and returns the
// resulting keys. Requires at least one key.
func DecodeKeys(a ...string) ([]*Key, error) {
if len(a) == 0 {
return nil, errNoKeys
}
var err error
ks := make([]*Key, len(a))
for i, s := range a {
ks[i], err = DecodeKey(s)
if err != nil {
return nil, err
}
}
return ks, nil
}
// MustDecodeKeys is like DecodeKeys, but panics if an error occurs.
// It simplifies safe initialization of global variables holding
// keys.
func MustDecodeKeys(a ...string) []*Key {
k, err := DecodeKeys(a...)
if err != nil {
panic(err)
}
return k
}

16
vendor/github.com/fernet/fernet-go/verify.json generated vendored Normal file
View File

@ -0,0 +1,16 @@
[
{
"token": "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==",
"now": "1985-10-26T01:20:01-07:00",
"ttl_sec": 60,
"src": "hello",
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
},
{
"token": "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==",
"now": "1985-10-26T01:20:01-07:00",
"ttl_sec": -1,
"src": "hello",
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
}
]