附录 2. 密码学专题 - 数学知识

密码学专题 - 数学知识

2. 数论

这里仅列出一些对密码学有用的思想,关于数论更详细的知识请参考专业文献。

2.1 模运算

本质上,如果 对某些整数 成立,那么 。如果 为正, 为 0 ~ n,那么你可将 看做 被 整除后的余数。有时 叫做 模 的余数 (residue)。有时 叫做与 模 同余 (congruent) (三元等号 表示同余)。

从 的整数组成的集合构成了模 的完全剩余集 (complete set of residue)。这意味着,对于每一个整数 ,它的模 的余项是从 的某个数。

模 的运算给出了 的余数,余数是从 的某个整数,这种运算称为模变换 (modular reduction)。例如,。

模运算就像普通运算一样,它是可交换的、可结合的和可分配的。而且,简化每一个中间结果的模 运算,其作用与先进行全部运算再简化模 运算是一样的。

密码学用了许多模 运算,因为像计算离散对数和平方根这样的问题很困难,而模运算可将所有中间结果和最后结果限制在一个范围内,所以用它进行计算比较容易。对一个 位的模数 ,任何加、减、乘的中间结果将不会超过 位长。因此可以用模运算进行指数运算而又不会产生巨大的中间结果。虽然计算某数的乘方并对其取模的运算

将导致一系列的乘法和除法运算,但有加速运算的方法:一种方法指在最小化模乘法运算的数量;另一种旨在优化单个模乘法运算。因为操作步骤划分后,当完成一串乘法,并且每次都进行模运算后,指数运算就更快,这样就与一般取幂没有多大差别,但当用 200 位的数字进行运行时,情况就不同了。

例如,如果要计算 ,不要直接进行七次乘法和一个大数的模化简:

相反,应进行三次较小的乘法和三次较小的模化简:

以此类推,

当 不是 2 的幂次方时,计算 稍微要难些。可将 表示成 2 的幂次方之和:在二进制中,25 是 11001,因此 。故:

注意,上面的公式利用了,。

适当利用存储的中间结果,只需要 6 次乘法:

这种算法称为加法链 (addition chaining),或二进制平方和乘法方法。它用二进制表示了一个简单明了的加法链。算法的 C 语言描述如下:

unsigned long qe2(unsigned long x, unsigned long y, unsigned long n) {
    unsigned long s, t, u;
    int i;

    s = 1; t = x; u = y;
    while (u)
    {
        if (u&1) s = (s * t) % n;
        u >>= 1;
        t = (t * t) % n;
    }
    
    return s;
}

另一种递归算法为:

unsigned long fast_exp(unsigned long x, unsigned long y, unsigned long N) {
    unsigned long tmp;
    if (y == 1) return (x % N);

    if ((y & 1) == 0) {
        tmp = fast_exp(x, y/2, N);
        return ((tmp * tmp) % N);
    }
    else {
        tmp = fast_exp(x, (y-1) / 2, N);
        tmp = (tmp * tmp) % N;
        tmp = (tmp * x) % N;
        return (tmp);
    }
}

对应的 python 实现如下。

def qe2(x, y, n):
    s = 1
    t = x
    u = y

    while (u):
        if (u&1):
            s = (s * t) % n
        u >>= 1
        t = (t * t) % n
    
    return s

另一种递归算法为:

def fast_exp(x, y, N):
    if (y == 1):
        return (x % N)

    if ((y & 1) == 0):
        tmp = fast_exp(x, y/2, N)
        return ((tmp * tmp) % N)
    else:
        tmp = fast_exp(x, (y-1) / 2, N)
        tmp = (tmp * tmp) % N
        tmp = (tmp * x) % N
        return (tmp)

如果用 表示数 中位数的长度,这项技术平均可减少 次操作。

2.2 整除性与素数

素数是这样一种数:比 1 大,其因子只有 1 和它本身,没有其他数可以整除它。2 是一个素数,其他的素数如 73、2521、2365347734399 和 等。素数是无限的。密码学,特别是公开密钥密码学常用大的素数 (512 位,甚至更大)。

如果 除以 余数为 0,则称 是 的一个因子 (记叙 ,读作 “a整除b”)。比如,7 是 35 的一个因子,记作 。如果一个数只有 1 和它自身两个正因子,我们就称这个数是素数。比如,13 是素数,两个因子为 1 和 13。最初几个素数很容易找到:2、3、5、7、11、13...... 如果一个整数大于 1 且不为素数,我们就称为合数。1 既不是素数也不是合数。

下面是关于整除性的一个简单的引理。

引理 1:如果 且 ,那么 。

证明:如果 ,那么存在整数 使得 (由 能被 整除可知 是 的倍数);如果 ,同样存在整数 使得 。综上可知,,所以 为 的一个因子。

引理 2:如果 为大于 1 的正整数且 为 除 1 之外最小的因子,那么 是素数。

证明:首先,我们必须保证 是被明确定义的。(如果对于某个 ,除 1 之外不存在一个最小的因子,那么 的定义就不恰当,引理 2 就毫无意义。) 由于 也是 的一个因子,而 ,所以 至少有一个大于 1 的因子,也必然有一个大于 1 的最小因子。

为证明 是素数,我们使用一种标准的数学技巧,称为反证法。为证明结论 X,反证法的一般思路是假设 X 不成立,接着从这个假设推出矛盾;如果假设 X 不成立能够推出矛盾,那么 X 必须是正确的。

在这个例子中,我们假设 不是素数,那么 肯定存在满足 的因子 。但是从引理 1 可知,如果 且 ,那么 ,即 也是 的一个因子且 。这样就产生了矛盾,因为 被定义为 除 1 之外最小的因子,因此我们的假设是错误的,从而 是素数。

定理 3 (殴几里得):素数有无穷多个。

证明:我们仍然使用反证法来证明。假设素数的个数是有限的,那么一个包含所有素数的列表也是有限的,记为 ,这里 表示素数的个数。定义 ,即 为所有素数的乘积加上 1。

考虑 除 1 之外的最小因子,我们仍用 来表示这个因子。由引理 2 可知, 为素数且 ;但是在那个有限的素数列表中,没有一个素数是 的因子,因为它们都是 的因子, 除以列表中任何一个素数 都会有余数 1,所以 为素数且不在列表中。而列表在定义时就包含了所有素数的,这样就出现了矛盾,所以素数的个数是有限的这个假设是错误的,从而可知素数有无穷多个。

2.3 最大公因子

两个数互素 (relatively prime) 是指:当它们除了 1 外没有共同的因子。换句话说,如果 和 的最大公因子 (greatest common divisor) 等于 1,那么可写作:

数 15 和 28 是互素的,15 和 27 不是,而 13 和 500 是。一个素数与它的倍数以外的任何其他数都是互素的。

计算两个数的最大公因子最容易的方法是用殴几里得算法 (Euclid's algorithm)。殴几里德在公元前 300 年所写的《Elements》中描述了这个算法。这个算法并非由他发明,历史学家相信这个算法在当时已有 200 年历史。它是幸存到现在最古老的非凡的算法,至今它仍是完好的。

算法的 C 语言描述如下:

/* returns gcd of x and y */
int gcd(int x, int y) {
    int g;

    if (x < 0)
        x = -x;
    
    if (y < 0)
        y = -y;

    if (x + y == 0)
        exit(1);

    g = y;

    while (x > 0)
    {
        g = x;
        x = y % x;
        y = g;
    }
    
    return g;
}

这个算法可以推广为返回由 m 个数组成的 gcd 数组。

/* return the gcd of x1, x2, ..., xm */
int multiple_gcd(int m, int *x) {
    size_t i;
    int g;

    if (m < 1)
        return 0;
    
    g = x[0];

    for (i = 1; i < m; ++i) {
        g = gcd(g, x[i]);

        /* optimization, since for random x(i), g==1 60% of the time: */
        if (g == 1)
            return 1;
    }

    return g;
}

对应的 python 实现如下。

# returns gcd of x and y
def gcd(x, y):
    if (x < 0):
        x = -x
    
    if (y < 0):
        y = -y

    if (x + y == 0):
        exit(1)

    g = y

    while (x > 0):
        g = x
        x = y % x
        y = g
    
    return g

这个算法可以推广为返回由 m 个数组成的 gcd 数组。

# return the gcd of x1, x2, ..., xm
def multiple_gcd(m, x):
    if (m < 1):
        return 0
    
    g = x[0]

    for i in range(m):
        g = gcd(g, x[i])

        # optimization, since for random x(i), g==1 60% of the time:
        if (g == 1):
            return 1

    return g

2.4 殴几里得算法

求最大公因子 (GCD) 的算法。

2.5 求模逆元

记得逆元 (inverse) 吗?4 的乘法逆元是 1/4,因为 。在模运算的领域,这个问题更复杂:

这个方程等价于寻找一组 和 ,以使:

这里 和 均为整数。

更为一般的问题是寻找一个 ,使得:

也可写作:

解决模的逆元问题很困难。有时候有一个方案,有时候没有。例如,5 模 14 的逆元是 3:。2 模 14 却没有逆元。

一般而论,如果 和 是互素的,那么 有唯一解;如果 和 不是互素的,那么 没有解。如果 是素数,那么从 的每一个数与 都是互素的,且在这个范围内恰好有一个逆元。

一切顺利。现在,怎样找出 模 的逆元呢?有一系列的方法。殴几里得算法也能计算 模 的逆元,有时候这叫做扩展殴几里得算法 (extended Euclidean algorithm)。

下面是用 C++ 写的算法:

#include 

#include 

using namespace std;

#define isEven(x)       ((x & 0x01) == 0)
#define isOdd(x)        (x & 0x01)
#define swap(x,y)       (x ^= y, y ^= x, x ^= y)

void ExtBinEuclid(int *u, int *v, int *u1, int *u2, int *u3) {
    // warning: u and v will be swapped if u < v
    int k, t1, t2, t3;

    if (*u < *v) swap(*u, *v);

    for (k = 0; isEven(*u) && isEven(*v); ++k) {
        *u >>= 1; *v >>= 1;
    }

    *u1 = 1; *u2 = 0; *u3 = *u; t1 = *v; t2 = *u - 1; t3 = *v;
    
    do {
        do {
            if (isEven(*u3)) {
                if (isOdd(*u1) || isOdd(*u2)) {
                    *u1 += *v; *u2 += *u;
                }

                *u1 >>= 1; *u2 >>= 1; *u3 >>= 1;
            }

            if (isEven(t3) || *u3 < t3) {
                swap(*u1, t1); swap(*u2, t2); swap(*u3, t3);
            }
        } while (isEven(*u3));
        
        while (*u1 < t1 || *u2 < t2) {
            *u1 += *v; *u2 += *u;
        }

        *u1 -= t1; *u2 -= t2; *u3 -= t3;
    } while (t3 > 0);
    
    while (*u1 >= *v && *u2 >= *u) {
        *u1 -= *v; *u2 -= *u;
    }
    
    *u1 <<= k; *u2 <<= k; *u3 <<= k;
}

int main(int argc, char **argv) {
    int a, b, gcd;

    if (argc < 3) {
        std::cerr << "Usage: xeuclid u v" << std::endl;
        
        return -1;
    }

    int u = atoi(argv[1]);
    int v = atoi(argv[2]);

    if (u <= 0 || v <= 0) {
        std::cerr << "Arguments must be positive!" << std::endl;

        return -2;
    }

    // warning: u and v will be swapped if u < v
    ExtBinEuclid(&u, &v, &a, &b, &gcd);

    std::cout << a << " * " << u << " + (-"
            << b << ") * " << v << " = " << gcd << std::endl;

    if (gcd == 1)
        std::cout << "the inverse of " << v << " mod " << u << " is: " << u - b << std::endl;
    
    return 0;
}

此算法通过迭代运算来实现,对于大的整数,其运行可能较慢。Knuth 指出这个算法完成的除法的平均数目是

2.6 求系数

殴几里得算法可用于解决下面的一类问题:给出一个包含 个变量 的数组,求一个包含 个系数 的数组,使得

2.7 费马小定理

如果 是一个素数,且 不是 的倍数,那么,根据费马小定理 (Fermat's little theorem) 有:

2.8 欧拉函数

还有另一种方法计算模 的逆元,但不是在任何情况下都能使用。模 的余数化简集 (reduced set of residues) 是余数完全集合的子集,与 互素。例如,模 12 的余数化简集是 。如果 是素数,那么模 的余数化简集是从 的所有整数集合。对 不等于 1 的数,数 0 不是余数化简集的元素。

欧拉函数 (Euler totient fuction),也称为欧拉 函数,写作 ,它表示模 的余数化简集中元素的数目。换句话说, 表示与 互素的小于 的正整数的数目 ()。

如果 是素数,那么 ;如果 ,且 和 互素,那么 。这些数字在随后谈到的公开密钥系统中将再次出现,它们都来自于此。

根据费马小定理的欧拉推广,如果 ,那么

现在计算 模 很容易:

现在计算 模 很容易:

证明:

例如,求 5 模 7 的逆元是多少?既然 7 是素数,。因此,5 模 7 的逆元是

计算逆元的两种方法都推广到在一般性的问题中求解 (如果 ):

用欧拉推广公式,解:

用殴几里得算法,解:

通常,殴几里得算法在计算逆元方面比欧拉推广更快,特别是对于 500 位范围内的数。如果 ,并非一切都没用了。这种一般情况而言,,可能有多个解或无解。

2.9 中国剩余定理

如果已知 的素因子,那么就能利用中国剩余定理 (Chinese remainder theorem) 求解整个方程组,这个定理的最初形式是由 1 世纪的中国数学家孙子发现的。

一般而言,如果 的素因子可分解为 ,那么方程组

有唯一解,这里 (注意,有些素数可能不止一次地出现。例如,p_1 可能等于 p_2)。换句话说,一个数 (小于一些素数之积) 被它的余数模这些素数唯一确定。

例如,取素数 3 和 5,取一个数 14,那么 。则小于 且具有上述余数的数只有 14,即由这两个余数唯一地确定了数 14。

如果对任意 和 ( 和 都是素数),那么,当 时,存在一个唯一的 ,使得

为求出这个 ,首先用殴几里得算法找到 ,使得

然后计算:

用 C 语言所写的中国剩余定理如下:

/* r is the number of elements in arrays m and u;
m is the array of (pairwise relatively prime) moduli
u is the array of coefficients
return value is n such than n == u[k]%m[k] (k=0..r-1) and
    n < m[0]*m[1]*...*m[r-1]
*/

/* totient() is left as an exercise to the reader. */

int chinese_remainder(size_t r, int *m, int *u) {
    size_t i;
    int modulus;
    int n;
    modulus = 1;
    for (i = 0; i < r; ++i)
        modulus *= m[i];
    n = 0;
    for (i = 0; i < r; ++i) {
        n += u[i] * modexp(modulus / m[i], totient(m[i]), m[i]);
        n %= modulus;
    }
    
    return n;
}

中国剩余定理的一个推论可用于求出一个类似问题的解:如果 和 都是素数,且 ,那么存在一个唯一的 ,使得

如果 ,那么

如果 ,那么

2.10 二次剩余

如果 是素数,且 ,如果

那么称 是对模 的二次剩余 (quadratic residue)。

不是所有的 的值都满足这个特性。如果 是对模 的一个二次剩余,那么它必定是对模 的所有素因子的二次剩余。例如,如果 ,那么二次剩余是 1、2 和 4:

注意,每一个二次剩余在上面都出现了两次。

没有 值可满足下列这些方程的任意一个:

对模 7 的二次非剩余 (quadratic nonresidue) 是 3、5 和 6。

很容易证明,当 为奇数时,对模 的二次剩余数目恰好是 ,且与其二次非剩余的数目相同。而且,如果 等于二次剩余模 ,那么 恰好有两个平方根:其中一个在 之间;另一个在 之间。这两个平方根中的一个也是模 的二次剩余,称为主平方根 (pricipal square root)。

如果 是两个素数 和 之积,那么模 恰好有 个二次剩余。模 的一个二次剩余是模 的一个完全平方。这是因为要成为模 的平方,其余数必须有模 的平方和模 的平方。例如,模 35 有 11 个二次剩余:1、4、9、11、14、15、16、21、25、29、30。每一个二次剩余恰好有 4 个平方根。

3. 有限域上的离散对数

模指数运算是频繁地用于密码学中的另一种单向函数。计算下面的表达式很容易:

模指数运算的逆问题是找出一个数的离散对数,这是一个难题:

例如:

不是所有的离散对数都有解 (记住,只有整数才是合法的解)。发现下面的方程没有解 很容易:

对 1024 位的数求离散对数更加困难。

3.1 计算有限群中的离散对数

密码设计者对下面三个主要群的离散对数很感兴趣:

  • 素数域的乘法群:。
  • 特征为 2 的有限域上的乘法群:。
  • 有限域 上的椭圆曲线群:。

许多公开密钥算法的安全性是基于寻找离散对数的,因此对这个问题进行了广泛的研究。

项目源代码

项目源代码会逐步上传到 Github,地址为 https://github.com/windstamp。

Contributor

  1. Windstamp, https://github.com/windstamp

你可能感兴趣的:(附录 2. 密码学专题 - 数学知识)