| 1 | package moves |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "math" |
| 7 | "time" |
| 8 | ) |
| 9 | |
| 10 | // SignatureLife is how long S3 accepts a SigV4 signature (15 minutes, |
| 11 | // verified §15 step 8). A staging writer may sign only while its lease is |
| 12 | // safe for that whole life plus the clock skew (§7.3, model 14, 18). |
| 13 | const SignatureLife = 15 * time.Minute |
| 14 | |
| 15 | // StagingLeaseTTL is the staging-work lease's term (D3). The ordinary |
| 16 | // lease.LeaseTTL is 15 minutes, which can never leave SignatureLife plus |
| 17 | // the skew in front of SafeUntil; 45 minutes gives every renewal cycle |
| 18 | // room, and the renewer keeps the bound ahead while the lane works. |
| 19 | const StagingLeaseTTL = 45 * time.Minute |
| 20 | |
| 21 | // DefaultSkew is the clock-skew allowance the guard subtracts. |
| 22 | const DefaultSkew = time.Minute |
| 23 | |
| 24 | // ErrLeaseUnsafe is the guard's refusal: the staging lease is too close to |
| 25 | // its end (or already lost) for another request to be signed. |
| 26 | var ErrLeaseUnsafe = errors.New("moves: staging lease too close to its end to sign another request") |
| 27 | |
| 28 | // StagingLeasePrefix is the claim's shared staging-work lease (claim C3, |
| 29 | // §7.3 D3): in the deployment bucket, keyed by target region and host, so |
| 30 | // a source that cannot read the target's lock.json still excludes other |
| 31 | // writers of the same lane. |
| 32 | func StagingLeasePrefix(targetRegion, host string) string { |
| 33 | return "control/staging-leases/" + targetRegion + "/" + host |
| 34 | } |
| 35 | |
| 36 | // ErrInvalidSkew is a negative Guarded.Skew: it would shrink the window |
| 37 | // the guard exists to keep (a -15 minute skew admits a signature that |
| 38 | // outlives the lease), so it is refused rather than honoured. |
| 39 | var ErrInvalidSkew = errors.New("moves: staging guard skew must not be negative") |
| 40 | |
| 41 | // SafeUntilGuard is the one method of *lease.Held the guard reads. |
| 42 | type SafeUntilGuard interface{ SafeUntil() (time.Time, error) } |
| 43 | |
| 44 | // Guarded wraps every staging request: the check runs immediately before |
| 45 | // fn signs, and fn signs AT THE CHECK'S TIMESTAMP, which Before hands it. |
| 46 | // A request that cannot complete within the signature's life before the |
| 47 | // lease ends is dropped; a request signed at the check and sent after any |
| 48 | // pause carries a date S3 stops accepting exactly when the guard's bound |
| 49 | // said it must. fn must issue exactly one request through a client that |
| 50 | // signs once, at signAt, and never retries (lease.StagingMirror's At |
| 51 | // methods); a caller that wants a retry calls Before again. |
| 52 | type Guarded struct { |
| 53 | Lease SafeUntilGuard |
| 54 | Now func() time.Time |
| 55 | Skew time.Duration // zero means DefaultSkew |
| 56 | } |
| 57 | |
| 58 | // Before refuses with ErrLeaseUnsafe unless the lease stays safe for the |
| 59 | // signature's whole life plus the skew from now, where now is read once |
| 60 | // and handed to fn as the signing time. lease.Held.SafeUntil already |
| 61 | // subtracts the renewal margin; Before subtracts the signature's life. |
| 62 | func (g Guarded) Before(fn func(signAt time.Time) error) error { |
| 63 | skew := g.Skew |
| 64 | if skew < 0 { |
| 65 | return ErrInvalidSkew |
| 66 | } |
| 67 | if skew == 0 { |
| 68 | skew = DefaultSkew |
| 69 | } |
| 70 | safe, err := g.Lease.SafeUntil() |
| 71 | if err != nil { |
| 72 | return fmt.Errorf("%w: %v", ErrLeaseUnsafe, err) |
| 73 | } |
| 74 | // A skew so large that SignatureLife+skew would overflow can never be |
| 75 | // satisfied: refuse rather than wrap into a pass. |
| 76 | if skew > math.MaxInt64-SignatureLife { |
| 77 | return ErrLeaseUnsafe |
| 78 | } |
| 79 | now := g.Now() |
| 80 | // safe.Sub saturates, so no sum of times can overflow; the comparison |
| 81 | // is the same as now+SignatureLife+skew being after safe. |
| 82 | if safe.Sub(now) < SignatureLife+skew { |
| 83 | return ErrLeaseUnsafe |
| 84 | } |
| 85 | return fn(now) |
| 86 | } |
| 87 | |