工作中遇到的RSA问题,这里或许能找到答案

RSA的疑惑

对于不是专门研究算法的同学来说,如果会跟RSA打交道,大部分情况都是做接口参数的签名生成或校验。对于37手游发行平台来说,因为要对接巨多渠道的接口,就会遇到各种各样的签名规则,每每碰到要求使用RSA签名,心里总会咯噔一下,并不是说使用RSA门槛会多高,主要是其中的细节会很多,如果写文档的人/对接的人不熟悉RSA,没讲清楚细节,那对接起来真想口吐芬芳,比如:

  • 只讲了用RSA生成签名,没说明签名类型

  • 验证签名要根据接口参数的序列号找到对应的证书公钥(一个接口会有不同私钥生成签名的情况)

  • 难度再升级,需要将接口参数携带的自签证书,验证一下证书链

即使不像我们会对接很多渠道,但任何公司的业务系统都会有支付系统,那肯定会对接支付渠道,比如云闪付、支付宝、微信、抖音等。

这时候你可能要喊:"打住打住,兄弟,RSA不是非对称加解密吗?怎么都是签名?"

且听我继续讲下去,这时候你是否还有其它疑问呢?

  • 对于加解密功能,有我擅长用的对称加密 AES不都够了吗
  • RSA是如何用不同密钥,实现加解密的能力的
  • 我们经常听到的是公钥加密消息,私钥解密获得原文,那么能不能反过来
  • 关于RSA,是不是认为它除了加解密,还有数字签名功能,其它加密/哈希算法一个能打的都没有,有RSA就够了
  • RSA的典型应用https,是怎么保障校验"我"就是"我"的 (可能没有这个疑问,毕竟老八股文了)

RSA为何而生

抛开场景讲功能,都是空中楼阁,其创造必定是为了解决它的前辈们(对称加密)解决不了的问题,RSA加密算法主要是解决了密钥交换的安全性问题。

对称加密算法需要在通信双方之间共享密钥,但在密钥分发方面存在困难,因为密钥是一样的,我提供给你,中间就有可能被拦截或后期发生泄露,整个通信的安全性就会受到威胁。

RSA算法使用了公钥和私钥的非对称性质,允许通信双方使用公钥进行加密和解密信息,避免了对密钥的直接传输,提供了更安全的密钥交换方式。即使公钥拦截或泄漏了,也只有私钥的持有方能够生成加密的消息,接收方只能解密正确加密的信息,而不是被伪造的消息。

RSA加解密原理

你会不会觉得很神奇,一把锁被一把钥匙锁住了,只能用另一把唯一的钥匙来开。对于加解密来说,本质上就是算法逻辑的计算,经过一轮捣鼓后又变回了原样。

从开发的逻辑考虑,一个内容可以被还原回去,肯定得像对称密钥一样知己知彼才能做到。就好比 em + d c = 1, m为原文,c为密文,要知道e和d,才能算出密文c。要把c计算得到m, 同样也得知道e和d。不能一方拿着e,另一方拿着d,大家虽然都知道表达式ec + dm = 1,但这样是没法马上算出的。那就是要有关联才能继续玩了,那一方拿着e和n,另一方拿着d和n呢,假设 n = e*d,那这样就可以算了,当然这例子看起来漏洞百出,相当于我有了公钥也能知道私钥的内容,何谈安全?

然而事实上,RSA就是类似这样,公钥包含了参数[n,e],私钥包含了参数[n,d],只不过RSA是用了不同数学公式,保证加解密是正确的,而且即使你知道n、e和d,彼此是有关联并且公开的,也很难通过你手里的公钥[n,e],算出d。

RSA加密的数学原理基于两个主要数论概念:大素数的乘积与模幂运算。RSA数学原理详细推导

  1. 大素数的乘积:选择两个大素数 p 和 q。计算它们的乘积 n = p * q,这个 n 就是 RSA 加密中的模数(modulus)。n 的大小决定了加密的强度和密钥的长度。
  2. 欧拉函数 φ(n):对于两个素数 p 和 q,它们的欧拉函数 φ(n) = (p - 1) * (q - 1)。φ(n) 表示小于 n 且与 n 互素的正整数个数。
  3. 选择公钥和私钥:选择一个与 φ(n) 互素的数 e,作为公钥的指数(encryption exponent)。然后,计算私钥的指数 d,使得 (d * e) % φ(n) = 1,d 为私钥的指数(decryption exponent)。
  4. 加密过程:对消息 m 进行加密时,利用公钥指数 e 对消息进行模幂运算,得到密文 c = m^e % n。
  5. 解密过程:接收到密文 c 后,使用私钥指数 d 对密文进行模幂运算,得到原始消息 m = c^d % n。

总结一下其中对我们最有用的信息,就是算法生成三个数字,用来生成公私钥,模n、公钥幂e、私钥幂d,可以用工具或者熟悉的编程语言,提取生成的公私钥里面的信息;截图可以看出公钥幂e比较小,而私钥d是一个16进制大整数

csharp 复制代码
// 生成私钥
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
// 生成公钥
openssl rsa -pubout -in private_key.pem -out public_key.pem

我们用比较小的数字来验证下这个加解密过程,n=77, e=13, d=37, 加密原文m=7(密文需要比n小,非数字则转成对应的10进制后计算,计算机底层是2进制;加密内容超过的话,RSA加密过程会进行分组)

公钥参数加密:c = m^e % n = 7 ^ 13 % 77 = 35

私钥解密:m = c^d % n = 35 ^ 37 % 77 = 7

不得不说数学是皇冠上的明珠。也就是说我们公钥只要有了[n,e],私钥是[n,d]就能实现加解密,前面我漏洞百出的例子提到过公私钥会有一定的关联,那RSA公钥[n,e]和私钥[n,d]的关联就是 e⋅d≡1(modϕ(n)) ,是实现加解密的关键,(其中加解密能实现的原理可以看RSA数学原理详细推导,对数学的知识水平要求不高)

从这个表达式 e⋅d≡1(modϕ(n)) 我们可以得出另外个结论,既然 c = m^e % n 能够推导出 m = c^d % n,那么反过来也是成立的 c = m^d % n 能够推导出 m = c^e % n

私钥加密:c = m^d % n = 7 ^ 37 % 77 = 28

公钥解密:m = c^e % n = 35 ^ 13 % 77 = 7

所以这里也解答了我们的一个疑问:"我们经常听到的是公钥加密消息,私钥解密获得原文,那么能不能反过来",答案是可以的,而且使用的算法还是一样的!

RSA的使用

  • 非对称加解密
  1. 原文使用公钥加密,使用私钥解密

  2. 对原文使用私钥加密,使用公钥解密

从前面原理我们可以知道,RSA就是提供了一个数据加密功能,那么我们是不是不要用AES这种对称加密算法,毕竟它有传输安全问题。然而事实上,我们很少使用RSA来做消息原文的加密,而且不推荐,因为这个加密/解密过程很耗CPU。从加解密过程,我们可以知道,都是指数级运算,而且指数还很大,加密原文换成2进制也是很大的。比如这个私钥的幂值就很大,做指数运算相当占CPU时间。如果你只是处理几个数据那还好,但在互联网时代,用https请求已经是一个基本的要求,流量的规模相当大,一般机器是扛不住的。

还记得我司在虚拟机时代,要上Https功能,还专门去调研卸载Https转Http的设备,得有专门的机器来搞。就像防火墙也有专门的设备F5。

所以,我们很少看到RSA去做消息原文的加密,更多是做数字签名。

  • 数字签名

数字签名是用于验证消息真实性和完整性的加密技术。它是一种加密机制,用发送者的私钥对消息进行加密,然后接收者可以使用发送者的公钥对签名进行解密验证。类似于我们经常用的 md5 / sha256 哈希函数,生成一个签名验证。

但是通过RSA的原理我们已经知道了,RSA本身只有加解密的能力,并不具备生成一个不可逆的签名值。那这是怎么回事呢?这其实是结合了哈希算法生成不可逆值能力和RSA传输安全的能力,也就是说封装了哈希和RSA,定义好了签名验证的标准,各个语言去实现就行了。

数字签名的过程通常如下:

  1. 消息哈希:消息首先会通过哈希算法(如SHA-256)生成一个固定长度的哈希值,不管消息的长度是多少,哈希值长度都是固定的。
  2. 数字签名:生成的哈希值会被用发送方的私钥进行签名,形成数字签名。RSA的私钥签名过程实际上是对哈希值进行填充和格式化后进行加密,确保只有持有对应公钥的人才能解密得到相应的哈希值。
  3. 验证签名:接收方使用发送方的公钥对数字签名进行解密,得到哈希值。然后,接收方对原始消息进行相同的哈希运算,生成一个新的哈希值,同样进行填充和格式化。如果两个值相同,则验证通过,表明消息未被篡改,并且确实是由发送方签名的。

RSA证书与证书链

如果交互的双方都是已经明确的对象了,比如生成了一对公私钥,王纸把公钥交给公主,公主是已经确认给公钥的就是王纸本人,而不是伪装的,那公主和王纸彼此就可以安全的通信,不用担心伪造的问题。但是如果王纸把公钥给到公主的路上,被巫女截获了,那么之后公主一直通信的对象就是巫女了。

所以公主需要确保公钥本身就是王纸的,才能用它来通信,比如除了公钥还配了王纸的家庭地址,并且防止伪造,还基于公钥和家庭地址,用私钥生成了一个数字签名,但这样还不够,因为家庭地址是公开的信息,一样可以用其它公钥钥生成一个。那问题就是无法自证,所以需要第三方来作证,是否是真实的。比如由民政局的私钥来生成一个证书,民政局会确认家庭地址里住的就是王纸本人,把王纸的公钥和家庭地址和数字签名放在了一起,然后王纸就把民政局发的证书给到公主,公主就会用民政局的公钥验证数字签名,如果能验证通过,就说明证书里的公钥是王纸的。

从中,我们可以发现跟RSA相关的概念就是 证书 和 第三方机构(CA), 而且证书包含公钥本身、证明(网站域名)、数字签名、有效期、序列号主要的关键信息。

比如37手游的一个证书信息(证书都是公开的,浏览器就可以下载)证书查看工具

证书本身还会分 CSR(证书签名请求) 和 CA签发证书,以及证书链。

也就是说要申请CA签发证书,首先你要先基于私钥文件生成一个csr文件,填写要求输入的信息即可。然后把CSR证书以及要授权的域名提交给CA,CA会检查域名进行确认。

csharp 复制代码
// 生成CSR
openssl req -new -key private_key.pem -out csr.pem

如果是只有CA这层,那你就会拿到一个根证书(1份)。但是为了方便管理,CA本身会授权其它机构做签发证书,形成一个证书链,所以一般情况我们拿到的就是其它机构的签发证书,以及CA签发给机构的根证书(两份)

如果是签发证书链的话,都需要把它放在服务器上,比如浏览器https交互过程会拿到服务器所有证书,组成证书链,逐一往上检查整个链条是否正确的。当然如果服务器上没放全,只放了其它机构的签发证书,那如果客户端不主动去找根证书,那么就会建立连接失败。

证书验证过程,比较关键就是验证证书本身是有效的,也就是公钥是可信任的,并且证书里的域名跟当前访问的域名是一致的或者是其子域名,这样保障校验"我"就是"我"的

总结

本文介绍了在工作中遇到的RSA疑惑,给大家进行解答。在证书这一节其实还有很多细节没讨论,比如证书的格式是怎样的,有什么坑会踩到。其中最主要的还是认识到RSA一个原理,这样就可以解释很多场景。

工具:

PHP代码指定p和q,生成公钥[n, e], 私钥[n, d]

php 复制代码
<?php
// 已知的值
$p = 7;
$q = 9;
$n = $p * $q;

$phi = ($p-1)*($q-1);
$data = findCoprime($phi);

echo "p: {$p}". PHP_EOL;
echo "q: {$q}". PHP_EOL;
echo "n: {$n}". PHP_EOL;
echo "ϕ(n): {$phi}". PHP_EOL;

foreach($data as $val) {
    echo '--- 符合的 -----'. PHP_EOL;
    echo "e: {$val}". PHP_EOL;
    $result = extendedEuclidean($val, $phi);
    list($x, $y, $gcd) = $result;

    if ($gcd == 1) {
        $d = $x % $phi;
        if ($d < 0) {
            $d += $phi; // 确保 d 是正数
        }
        echo "d: $d" . PHP_EOL;
    } else {
        echo "无解" . PHP_EOL;
    }
}

function extendedEuclidean($a, $b) {
    $x = 0; $y = 1; $lastX = 1; $lastY = 0; $temp;
    
    while ($b != 0) {
        $quotient = intdiv($a, $b);
        
        $temp = $a % $b;
        $a = $b;
        $b = $temp;
        
        $temp = $x;
        $x = $lastX - $quotient * $x;
        $lastX = $temp;
        
        $temp = $y;
        $y = $lastY - $quotient * $y;
        $lastY = $temp;
    }
    
    return [$lastX, $lastY, $a];
}

// 与n互质的最小数
function gcd($a, $b) {
    while ($b != 0) {
        $temp = $a % $b;
        $a = $b;
        $b = $temp;
    }
    return $a;
}

function findCoprime($n) {
    $data = [];
    for ($i = 4; $i < $n; $i++) {
        if (gcd($i, $n) == 1) {
            $data[] = $i;
        }
    }
    return $data;
}

测试加解密,因为涉及指数计算生成大整数,要使用专门的库处理

go 复制代码
package main

import (
  "fmt"
  "math/big"
)


func main() {
	encrypt(13, 37, 77, 7)
	encrypt(37, 13, 77, 7)
}

func encrypt(e, d, n int64, m int64) {
	// 创建大整数 M, e, N
	M := big.NewInt(m)
	E := big.NewInt(e)
	N := big.NewInt(n)
	D := big.NewInt(d)

	// 创建一个用于存储结果的的大整数
	result := big.NewInt(0)

	// 执行幂运算并取模
	result.Exp(M, E, N)

	fmt.Println("加密:", result) // 输出结果

	deRes := big.NewInt(0)
	deRes.Exp(result, D, N)
	fmt.Println("解密:", deRes) // 输出结果
	if m == deRes.Int64() {
		fmt.Println("解密成功")
	} else {
		fmt.Println("解密失败")
	}
	println()
}
相关推荐
浮生如梦_2 小时前
Halcon基于laws纹理特征的SVM分类
图像处理·人工智能·算法·支持向量机·计算机视觉·分类·视觉检测
paopaokaka_luck3 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
励志成为嵌入式工程师4 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉4 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer4 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
码农小旋风4 小时前
详解K8S--声明式API
后端
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml45 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~5 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616885 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端