Browse Questions

Answer: A signature proves who sent a payload but says nothing about when, so an attacker who captures a valid signed request can resend it verbatim and it will still verify. The fix is to put a freshness value inside the signed body — a timestamp or a server-issued nonce — and have the server reject anything stale or already seen.

Sign the JSON after injecting the timestamp, never as a separate header. A value outside the signed bytes can be rewritten by anyone in the middle, which defeats the whole mechanism. On the server, reject requests outside a tight clock window and keep a short-lived cache of recently seen nonces so a request inside the window cannot be replayed either.

Code Example:

extension Encodable {
    /// Injects a timestamp into the JSON, then signs the whole body.
    func signedPayload() throws -> String {
        let data = try JSONEncoder().encode(self)
        var dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:]
        dict["timestamp"] = Int(Date().timeIntervalSince1970 * 1000)

        let withTimestamp = try JSONSerialization.data(withJSONObject: dict)
        guard let json = String(data: withTimestamp, encoding: .utf8) else {
            throw CryptoError.encoding
        }
        return try CryptoService.sign(message: json)   // TSMessage sign mode
    }
}

Key Points:

  • The freshness value must be inside the signed bytes, not a sibling header.
  • Pair a clock-skew window with a seen-nonce cache; neither alone is sufficient.
  • Device clocks drift and users change them — never trust the client timestamp as truth, only as a bound.