package runs import ( "crypto/sha256" "encoding/binary" "encoding/hex" "hash" "strings" "time" ) // A keyed create's bounds (service runs spec ยง 3). Each admission prunes the // record's run_keys to entries younger than KeyTTL, then keeps the newest // MaxRunKeysPerCaller entries of each caller. So a key is honoured for // KeyTTL, or until that same caller makes MaxRunKeysPerCaller newer keyed // admissions on this instance, whichever comes first. One caller's // admissions never evict another's key. A caller must not retry a key past // that: it is a fresh run then. const ( KeyTTL = 24 * time.Hour MaxRunKeysPerCaller = 32 // MaxKeyBytes is the longest Idempotency-Key a caller may send. MaxKeyBytes = 128 ) // ValidKey reports whether k is an Idempotency-Key: 1 to 128 bytes of // [A-Za-z0-9._:-]. func ValidKey(k string) bool { if len(k) == 0 || len(k) > MaxKeyBytes { return false } for i := 0; i < len(k); i++ { switch c := k[i]; { case 'a' <= c && c <= 'z', 'A' <= c && c <= 'Z', '0' <= c && c <= '9': case c == '.', c == '_', c == ':', c == '-': default: return false } } return true } // ScopedKey is the key as the record stores it: hex(sha256(scope, NUL, // key)). scope names the caller ("svc:" and a credential id, or "user:" // and an address), so one caller's key can never find, probe or collide // with another's, and the record never holds the caller's own words. func ScopedKey(scope, key string) string { h := sha256.Sum256([]byte(scope + "\x00" + key)) return hex.EncodeToString(h[:]) } // CallerOf is the caller's name as a run_keys entry stores it: // hex(sha256("caller", NUL, scope)). It groups a caller's entries for the // per-caller cap without the record holding the caller's identity. func CallerOf(scope string) string { h := sha256.Sum256([]byte("caller\x00" + scope)) return hex.EncodeToString(h[:]) } // ValidFileHash reports whether h is a sha256 as a create may declare it: 64 // hex digits, in either case. The console lowercases it (NormalFileHash) // before comparing it or digesting it. func ValidFileHash(h string) bool { if len(h) != 64 { return false } for i := 0; i < len(h); i++ { if c := h[i]; !('0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F') { return false } } return true } // NormalFileHash is a declared sha256 in the lowercase form the console // computes. func NormalFileHash(h string) string { return strings.ToLower(h) } // MetaDigest is the hex sha256 of a canonical encoding of what a create // asks for: argv, timeout_s, on_behalf_of, and each file's name, size and // sha256 (lowercased), in meta's order. Every string and count is // length-prefixed, so no two different requests share an encoding. A retry // with the same key must have the same digest. func MetaDigest(m Meta) string { h := sha256.New() str(h, "carlos-run-meta-v1") num(h, uint64(len(m.Argv))) for _, a := range m.Argv { str(h, a) } num(h, uint64(m.TimeoutS)) str(h, m.OnBehalfOf) num(h, uint64(len(m.Files))) for _, f := range m.Files { str(h, f.Name) num(h, uint64(f.Size)) str(h, NormalFileHash(f.SHA256)) } return hex.EncodeToString(h.Sum(nil)) } func num(h hash.Hash, n uint64) { var b [8]byte binary.BigEndian.PutUint64(b[:], n) h.Write(b[:]) } func str(h hash.Hash, s string) { num(h, uint64(len(s))) h.Write([]byte(s)) }