Hacker News new | past | comments | ask | show | jobs | submit login

Does the compiler guarantee constant time? If not, it's still useless for cryptography. If it does, then it becomes useless for regular work because plain bigint will kick it's ass on performance, especially when doing division.

This is an efficiency hack for fpga.




Compilers generally never guarantee constant time. The C and C++ standards have no notion of observing timing.


Tangentially related: C++ does have algorithmic complexity guarantees. For instance, std::unordered_map at() and operator[] are guaranteed to be average case complexity of O(1), and std::nth_element is guaranteed to be average case O(n).


Complexity guarantees for algorithms are often expressed explicitly in numbers of operations, not time [1].

Edit: actually, nth_element is a great example here: https://eel.is/c++draft/alg.nth.element#5

The complexity requirements for container member functions are often more vague an implicit, but I believe those are also not expressed in terms of time either. It's some other implicitly implied operation. In [2] for example this operation is the construction of a single object.

[1] https://eel.is/c++draft/sort#5 [2] https://eel.is/c++draft/sequences#vector.cons-5

An other tangentially related part of C++ is <chrono>, but I do not think that there are strict requirements there for precision. At most there is monotonic requirement for steady_clock[3].

[3] https://en.cppreference.com/w/cpp/chrono/steady_clock


What is observing timing?


One could imagine an annotation for if-statements that both branches should run for the same number of instructions or cycles.


I meant "observation of timing".


Constant-time division is super slow for sure but it's not needed for cryptography.

Modulo is also similarly slow but rarely needed as well. For example in elliptic curve cryptography it's only needed at the very beginning of a computation when hashing to the elliptic curve or when producing a secret key from a random input key material.

In terms of speed, LLVM iXYZ wide-integer code is usually faster for basic operations (modular addition, substraction, multiplication) the big slowness is for modular inversion where constant-time inversion is about 20x slower than GMP inversion.

Source: https://github.com/herumi/mcl#how-to-build-without-gmp this code has GMP backend, LLVM i254 / LLVM i381 backend, or it's own JIT compiler that uses the MULX, ADCX and ADOX instructions.

Plain bigint in general, including GMP are bottlenecked by memory allocation and the iXXX from LLVM are purely stack-based.

Regarding constant-time, I know that there have been petition to provide a constant-time flag to LLVM but no guarantees so far. Unfortunately you have to take an after the fact verification approach today unless you dropdown to assembly (see https://github.com/mratsim/constantine/wiki/Constant-time-ar... on a couple of constant-time verifiers available)


> or it's own JIT compiler that uses the MULX, ADCX and ADOX instructions.

Does LLVM's bigint implementation use those instructions? Last I tried LLVM can not handle the required data-dependencies on individual carry bits, making it impossible to use ADCX/ADOX in LLVM without inline assembly.


LLVM properly generates ADC code when you use __addcarry_u64 contrary to GCC[1] which generates really dumb code with setc/mov in and out of carry. However __addcarryx_u64 which is supposed to generate ADCX and ADOX is not properly handled and only generates ADC.

For the iXYZ themselves, the carries are properly handled and it can generate mulx at least: https://github.com/herumi/mcl/blob/master/src/asm/x86-64.bmi... (from straight LLVM IR). I don't think ADCX/ADOX are possible though even in LLVM IR.

I think you are mixing with one comment on the GCC mailing list about GCC not having the adequate representation of carry and even less a representation that can separate a carry and overflow chains[2]

[1] https://gcc.godbolt.org/z/2h768y [2] https://gcc.gnu.org/legacy-ml/gcc-help/2017-08/msg00100.html


There's a nearly identical issue in LLVM [1]. So neither are currently able to handle ADCX/ADOX in their IR and can only support it through inline assembly.

I was hoping LLVM could maybe bypass this IR limitation for the iXYZ types and generate the optimal instruction sequence, but from your assembly it looks like that doesn't happen either.

[1] https://bugs.llvm.org/show_bug.cgi?id=41546#c1


> Does the compiler guarantee constant time? If not, it's still useless for cryptography.

Side-channel resistant algorithms are only required when you are handling sensitive data. This is often not the case when you are verifying signatures, verifying proofs or generating succinct proofs of computation without a zero-knowledge requirement.


I'm somewhat confused. Constant w.r.t to what exactly? You can't have constant time operations on arbitrary bit length integers and once you fix the one parameter you have I fail to see what 'constant' means.


Constant time operations in the context of cryptography means that the runtime of the operation is not dependent on the data being manipulated.

As an example, suppose you were checking a 256-bit number for equality. This would be unacceptable:

    bool eq256(uint32_t a[8], uint32_t b[8]) {
        for (int i = 0; i < 8; ++i) {
            if (a[i] != b[i]) return false;
        }
        return true;
    }
Why? Because depending on the data this function takes longer or shorter, and timing attacks might be used to figure out secret data. Instead you need an implementation like this:

    bool eq256(uint32_t a[8], uint32_t b[8]) {
        uint32_t diff = 0;
        for (int i = 0; i < 8; ++i) {
            diff |= a[i] ^ b[i];
        }
        return diff == 0;
    }
This implementation always takes the same amount of time regardless of the contents of a and b.

It goes further than this as well, you're not supposed to take branches based on secret data nor access memory locations based on secret data as those can be recovered through the branch predictor and cache respectively.


There is no reason that an optimizing compiler doesn't "optimize" your function to non-constant time.


While downvoted at the time of my response, this answer is correct. The optimizing compiler makes no timing guarantees, so it's not required to use any of the constructs in your second answer at all. If you need guaranteed timing, you cannot use standard C (instead use assembly or compiler extensions).


A proper implementation uses the correct compiler intrinsics and hints to cause such operations to be constant-time. Timing problems are a fairly well-known problem in cryptography and constant-time implementations are often required for safe cryptography.


Often that hints boils down to use assembly though.

Constant-time is a pain to achieve and new compiler optimizations might make your code not constant-time anymore: https://www.cl.cam.ac.uk/~rja14/Papers/whatyouc.pdf

Given that I'm writing such a cryptographic library, I am very interesting on the compiler hints you use.


Fair enough. I just wanted to point out that standard C is inadequate on its own. I'm pretty sure that cryptographers are aware of this.


Oh right, that's what's being meant. I confused it with constant time as in O(1).

Wouldn't most implementations of fixed length integers have constant time though? It barely seems worth optimizing unless your integers vary massively in size, at which point using fixed size integers is clearly suboptimal.


I have an in-depth look on constant-time and the security implications of not being constant-time here:

https://github.com/mratsim/constantine/wiki/Constant-time-ar...



Constant time w.r.t the input values for a given bit length. For example a multiplier that is faster when one of the value is 0 would not work.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: