package moves import ( "errors" "fmt" "math" "time" ) // SignatureLife is how long S3 accepts a SigV4 signature (15 minutes, // verified §15 step 8). A staging writer may sign only while its lease is // safe for that whole life plus the clock skew (§7.3, model 14, 18). const SignatureLife = 15 * time.Minute // StagingLeaseTTL is the staging-work lease's term (D3). The ordinary // lease.LeaseTTL is 15 minutes, which can never leave SignatureLife plus // the skew in front of SafeUntil; 45 minutes gives every renewal cycle // room, and the renewer keeps the bound ahead while the lane works. const StagingLeaseTTL = 45 * time.Minute // DefaultSkew is the clock-skew allowance the guard subtracts. const DefaultSkew = time.Minute // ErrLeaseUnsafe is the guard's refusal: the staging lease is too close to // its end (or already lost) for another request to be signed. var ErrLeaseUnsafe = errors.New("moves: staging lease too close to its end to sign another request") // StagingLeasePrefix is the claim's shared staging-work lease (claim C3, // §7.3 D3): in the deployment bucket, keyed by target region and host, so // a source that cannot read the target's lock.json still excludes other // writers of the same lane. func StagingLeasePrefix(targetRegion, host string) string { return "control/staging-leases/" + targetRegion + "/" + host } // ErrInvalidSkew is a negative Guarded.Skew: it would shrink the window // the guard exists to keep (a -15 minute skew admits a signature that // outlives the lease), so it is refused rather than honoured. var ErrInvalidSkew = errors.New("moves: staging guard skew must not be negative") // SafeUntilGuard is the one method of *lease.Held the guard reads. type SafeUntilGuard interface{ SafeUntil() (time.Time, error) } // Guarded wraps every staging request: the check runs immediately before // fn signs, and fn signs AT THE CHECK'S TIMESTAMP, which Before hands it. // A request that cannot complete within the signature's life before the // lease ends is dropped; a request signed at the check and sent after any // pause carries a date S3 stops accepting exactly when the guard's bound // said it must. fn must issue exactly one request through a client that // signs once, at signAt, and never retries (lease.StagingMirror's At // methods); a caller that wants a retry calls Before again. type Guarded struct { Lease SafeUntilGuard Now func() time.Time Skew time.Duration // zero means DefaultSkew } // Before refuses with ErrLeaseUnsafe unless the lease stays safe for the // signature's whole life plus the skew from now, where now is read once // and handed to fn as the signing time. lease.Held.SafeUntil already // subtracts the renewal margin; Before subtracts the signature's life. func (g Guarded) Before(fn func(signAt time.Time) error) error { skew := g.Skew if skew < 0 { return ErrInvalidSkew } if skew == 0 { skew = DefaultSkew } safe, err := g.Lease.SafeUntil() if err != nil { return fmt.Errorf("%w: %v", ErrLeaseUnsafe, err) } // A skew so large that SignatureLife+skew would overflow can never be // satisfied: refuse rather than wrap into a pass. if skew > math.MaxInt64-SignatureLife { return ErrLeaseUnsafe } now := g.Now() // safe.Sub saturates, so no sum of times can overflow; the comparison // is the same as now+SignatureLife+skew being after safe. if safe.Sub(now) < SignatureLife+skew { return ErrLeaseUnsafe } return fn(now) }