在C

时间:2016-04-21 10:47:51

标签: c

免责声明:我很清楚实施自己的加密是一个非常糟糕的主意。这是硕士论文的一部分,代码不会在实践中使用。

作为更大的加密算法的一部分,我需要对一个恒定长度的数组(小的,精确的24)进行排序,而不会泄漏有关该数组内容的任何信息。据我所知(如果这些不足以防止时间和缓存攻击,请纠正我),这意味着:

  1. 无论数组的特定值如何,排序都应按照数组的长度以相同的周期数运行
  2. 排序不应分支或访问内存,具体取决于数组的特定值
  3. 是否存在任何此类实施?如果没有,这种类型的编程是否有任何好的资源?

    老实说,我甚至在更容易的子问题上苦苦挣扎,即找到数组的最小值。

    double arr[24]; // some input
    double min = DBL_MAX;
    
    int i;
    for (i = 0; i < 24; ++i) {
        if (arr[i] < min) {
            min = arr[i];
        }
    }
    

    添加带有虚拟分配的else是否足以使其具有时间安全性?如果是这样,我如何确保编译器(在我的情况下是GCC)不会撤消我的辛勤工作?这会容易受到缓存攻击吗?

3 个答案:

答案 0 :(得分:3)

使用分拣网络,一系列比较和交换。

交换调用不得依赖于比较。无论比较结果如何,都必须以执行相同数量指令的方式实现。

像这样:

void swap( int* a , int* b , bool c )
{
    const int min = c ? b : a;
    const int max = c ? a : b;
    *a = min;
    *b = max;  
}

swap( &array[0] , &array[1] , array[0] > array[1] );

然后找到分拣网络并使用交换。这是一个为您执行此操作的生成器:http://pages.ripco.net/~jgamble/nw.html

4个元素的示例,数字是数组索引,由上面的链接生成:

SWAP(0, 1);
SWAP(2, 3);
SWAP(0, 2);
SWAP(1, 3);
SWAP(1, 2);

答案 1 :(得分:2)

这是一种非常愚蠢的冒泡排序,它实际上有效,并且根据输入数据不会分支或更改内存访问行为。不确定这是否可以插入另一种排序算法,他们需要将它们与掉期分开,但也许它可能,现在正在进行。

#include <stdint.h>

static void
cmp_and_swap(uint32_t *ap, uint32_t *bp)
{
        uint32_t a = *ap;
        uint32_t b = *bp;
        int64_t c = (int64_t)a - (int64_t)b;
        uint32_t sign = ((uint64_t)c >> 63);
        uint32_t min = a * sign + b * (sign ^ 1);
        uint32_t max = b * sign + a * (sign ^ 1);
        *ap = min;
        *bp = max;
}

void
timing_sort(uint32_t *arr, int n)
{
        int i, j;
        for (i = n - 1; i >= 0; i--) {
                for (j = 0; j < i; j++) {
                        cmp_and_swap(&arr[j], &arr[j + 1]);
                }
        }
}

cmp_and_swap函数编译为(Apple LLVM版本7.3.0(clang-703.0.29),使用-O3编译):

_cmp_and_swap:
00000001000009e0        pushq   %rbp
00000001000009e1        movq    %rsp, %rbp
00000001000009e4        movl    (%rdi), %r8d
00000001000009e7        movl    (%rsi), %r9d
00000001000009ea        movq    %r8, %rdx
00000001000009ed        subq    %r9, %rdx
00000001000009f0        shrq    $0x3f, %rdx
00000001000009f4        movl    %edx, %r10d
00000001000009f7        negl    %r10d
00000001000009fa        orl     $-0x2, %edx
00000001000009fd        incl    %edx
00000001000009ff        movl    %r9d, %ecx
0000000100000a02        andl    %edx, %ecx
0000000100000a04        andl    %r8d, %edx
0000000100000a07        movl    %r8d, %eax
0000000100000a0a        andl    %r10d, %eax
0000000100000a0d        addl    %eax, %ecx
0000000100000a0f        andl    %r9d, %r10d
0000000100000a12        addl    %r10d, %edx
0000000100000a15        movl    %ecx, (%rdi)
0000000100000a17        movl    %edx, (%rsi)
0000000100000a19        popq    %rbp
0000000100000a1a        retq
0000000100000a1b        nopl    (%rax,%rax)

只有内存访问才能读取和写入数组,没有分支。编译器确实弄清了乘法实际上做了什么,实际上非常聪明,但它并没有使用分支。

强制转换为int64_t是必要的。我很确定它可以写得更干净。

根据要求,这里是双打比较功能:

void
cmp_and_swap(double *ap, double *bp)
{
        double a = *ap;
        double b = *bp;
        int sign = signbit(a - b);
        double min = a * sign + b * (sign ^ 1);
        double max = b * sign + a * (sign ^ 1);
        *ap = min;
        *bp = max;
}

编译代码是无分支的,根据输入数据不会改变内存访问模式。

答案 2 :(得分:0)

非常简单,时间常数(但也非常高效)排序

  • 有一个src和目标数组
  • 对于(已排序的)目标数组中的每个元素,遍历完整的源数组以查找完全属于此位置的元素。

没有早期休息,(几乎)不变的时间,不依赖于源的部分排序。