| 1 | package runs |
| 2 | |
| 3 | import ( |
| 4 | "crypto/sha256" |
| 5 | "encoding/binary" |
| 6 | "encoding/hex" |
| 7 | "hash" |
| 8 | "strings" |
| 9 | "time" |
| 10 | ) |
| 11 | |
| 12 | // A keyed create's bounds (service runs spec § 3). Each admission prunes the |
| 13 | // record's run_keys to entries younger than KeyTTL, then keeps the newest |
| 14 | // MaxRunKeysPerCaller entries of each caller. So a key is honoured for |
| 15 | // KeyTTL, or until that same caller makes MaxRunKeysPerCaller newer keyed |
| 16 | // admissions on this instance, whichever comes first. One caller's |
| 17 | // admissions never evict another's key. A caller must not retry a key past |
| 18 | // that: it is a fresh run then. |
| 19 | const ( |
| 20 | KeyTTL = 24 * time.Hour |
| 21 | MaxRunKeysPerCaller = 32 |
| 22 | // MaxKeyBytes is the longest Idempotency-Key a caller may send. |
| 23 | MaxKeyBytes = 128 |
| 24 | ) |
| 25 | |
| 26 | // ValidKey reports whether k is an Idempotency-Key: 1 to 128 bytes of |
| 27 | // [A-Za-z0-9._:-]. |
| 28 | func ValidKey(k string) bool { |
| 29 | if len(k) == 0 || len(k) > MaxKeyBytes { |
| 30 | return false |
| 31 | } |
| 32 | for i := 0; i < len(k); i++ { |
| 33 | switch c := k[i]; { |
| 34 | case 'a' <= c && c <= 'z', 'A' <= c && c <= 'Z', '0' <= c && c <= '9': |
| 35 | case c == '.', c == '_', c == ':', c == '-': |
| 36 | default: |
| 37 | return false |
| 38 | } |
| 39 | } |
| 40 | return true |
| 41 | } |
| 42 | |
| 43 | // ScopedKey is the key as the record stores it: hex(sha256(scope, NUL, |
| 44 | // key)). scope names the caller ("svc:" and a credential id, or "user:" |
| 45 | // and an address), so one caller's key can never find, probe or collide |
| 46 | // with another's, and the record never holds the caller's own words. |
| 47 | func ScopedKey(scope, key string) string { |
| 48 | h := sha256.Sum256([]byte(scope + "\x00" + key)) |
| 49 | return hex.EncodeToString(h[:]) |
| 50 | } |
| 51 | |
| 52 | // CallerOf is the caller's name as a run_keys entry stores it: |
| 53 | // hex(sha256("caller", NUL, scope)). It groups a caller's entries for the |
| 54 | // per-caller cap without the record holding the caller's identity. |
| 55 | func CallerOf(scope string) string { |
| 56 | h := sha256.Sum256([]byte("caller\x00" + scope)) |
| 57 | return hex.EncodeToString(h[:]) |
| 58 | } |
| 59 | |
| 60 | // ValidFileHash reports whether h is a sha256 as a create may declare it: 64 |
| 61 | // hex digits, in either case. The console lowercases it (NormalFileHash) |
| 62 | // before comparing it or digesting it. |
| 63 | func ValidFileHash(h string) bool { |
| 64 | if len(h) != 64 { |
| 65 | return false |
| 66 | } |
| 67 | for i := 0; i < len(h); i++ { |
| 68 | if c := h[i]; !('0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F') { |
| 69 | return false |
| 70 | } |
| 71 | } |
| 72 | return true |
| 73 | } |
| 74 | |
| 75 | // NormalFileHash is a declared sha256 in the lowercase form the console |
| 76 | // computes. |
| 77 | func NormalFileHash(h string) string { return strings.ToLower(h) } |
| 78 | |
| 79 | // MetaDigest is the hex sha256 of a canonical encoding of what a create |
| 80 | // asks for: argv, timeout_s, on_behalf_of, and each file's name, size and |
| 81 | // sha256 (lowercased), in meta's order. Every string and count is |
| 82 | // length-prefixed, so no two different requests share an encoding. A retry |
| 83 | // with the same key must have the same digest. |
| 84 | func MetaDigest(m Meta) string { |
| 85 | h := sha256.New() |
| 86 | str(h, "carlos-run-meta-v1") |
| 87 | num(h, uint64(len(m.Argv))) |
| 88 | for _, a := range m.Argv { |
| 89 | str(h, a) |
| 90 | } |
| 91 | num(h, uint64(m.TimeoutS)) |
| 92 | str(h, m.OnBehalfOf) |
| 93 | num(h, uint64(len(m.Files))) |
| 94 | for _, f := range m.Files { |
| 95 | str(h, f.Name) |
| 96 | num(h, uint64(f.Size)) |
| 97 | str(h, NormalFileHash(f.SHA256)) |
| 98 | } |
| 99 | return hex.EncodeToString(h.Sum(nil)) |
| 100 | } |
| 101 | |
| 102 | func num(h hash.Hash, n uint64) { |
| 103 | var b [8]byte |
| 104 | binary.BigEndian.PutUint64(b[:], n) |
| 105 | h.Write(b[:]) |
| 106 | } |
| 107 | |
| 108 | func str(h hash.Hash, s string) { |
| 109 | num(h, uint64(len(s))) |
| 110 | h.Write([]byte(s)) |
| 111 | } |
| 112 | |