密码学入门简明指南

这篇文章最后更新的时间在六个月之前,文章所叙述的内容可能已经失效,请谨慎参考!

这篇文章不涉及密码学的数学原理,只提及相关概念和应用。 文章的标题改成 应用密码学 可能会更好一点

相关的概念

信息安全

信息安全五要素

各个密码学概念对应的要素

各种攻击所对应的要素

以 https 为例解释信息安全的五要素

信息安全常识

  1. 不要使用保密的密码算法
  2. 使用低强度的密码比不进行任何加密更危险
    • 低强度的密码和没有加密同样不安全。但是使用了密码会给用户一种错误的安全感,导致用户容易泄露一些机密的信息。
  3. 任何密码总有一天都会被破解
  4. 密码只是信息安全的一部分

3A

信息安全的基本原则

为了达到信息安全的目标,各种信息安全技术的使用必须遵守一些基本的原则。

随机数

随机数的性质

  1. 随机性,不存在统计学偏差,是完全杂乱的数列
  2. 不可预测性,不能从过去的数列推测出下一个出现的数列
  3. 不可重现性,除非将数列本身保存下来,否则不能重现相同的数列

随机数的分类

随机数的作用

  1. 生成密钥
    • 用于对称密码和消息认证码
  2. 生成公钥密码
    • 用于生成公钥密码和数字签名
  3. 生成初始化向量 IV
    • 用于分组密码中的 CBC、CFB、OFB 模式
  4. 生成 nonce
    • 用于防御重放攻击和分组密码中的 CTR 模式
  5. 生成盐
    • 用于基于口令密码的 PBE 等

在 linux 下生成随机数

/dev/random 在类 UNIX 系统中是一个特殊的设备文件,可以用作随机数生成器。

/dev/random 的随机数的提供是依赖与外部中断事件的,如果没有足够多中断事件,就会阻塞。 /dev/random 生成的是真随机数。

/dev/urandom(“unblocked”,非阻塞的随机数生成器)是 /dev/random 的一个副本 ,它会重复使用熵池中的数据以产生伪随机数据。 这表示对 /dev/urandom 的读取操作不会产生阻塞,但其输出的熵可能小于 /dev/random 的。 它可以作为生成较低强度密码的伪随机数生成器,不建议用于生成高强度长期密码。

/dev/random 和 /dev/urandom 会输出二进制数据流,可以用 od 命令转换,或者用 base64 命令转换。

/dev/random 和 /dev/urandom 生成的都是符合密码学安全的随机数。因为 /dev/random 可能会阻塞,所以大部分情况下用 /dev/urandom 就可以了。

大部分情况下都是用 TRNG 生成的随机数作为种子,然后再用 CSPRNG 生成密码学安全的随机数。这样既能保证安全也能效率也不会太低。

命令行下的使用示例

# 不能直接用 cat ,因为 /dev/random 会一直输出
head -n 1 /dev/random | od -x
head -n 1 /dev/urandom | od -x
# 生成随机字符串
head -n 1 /dev/urandom | base64 | head -n 1
head -n 1 /dev/urandom | base64 | head -n 1
# 只生成数字
head -n 1 /dev/urandom | base64 | head -n 1 | tr -dc '0-9'
# 环境变量里的 $RANDOM 是一个随机数字
echo $RANDOM

在 windows 下生成随机数

windows api

bat 和 powershell

bat

echo %random%

powershell

// 生成一个随机数
Get-Random
// 生成一个小于等于 100 的随机数
Get-Random -Maximum 100
// 在这个范围内 [10.7, 20.93] 生成一个随机数
Get-Random -Minimum 10.7 -Maximum 20.93
// 在这个范围内 [1, 2, 3, 5, 8, 13] 随机选择 3 个数
Get-Random -InputObject 1, 2, 3, 5, 8, 13 -Count 3

x86 汇编生成随机数

RDRAND 指令 和 RDSEED 指令

RDRAND 是英特尔 x86 cpu 中一条用于生成真随机数的指令。

英特尔在 Ivy Bridge 微架构中内置了一个利用电阻热噪声取得硬件真随机数的功能。 后续的 x86 cpu 都支持这个指令。 Ivy Bridge 就是第三代酷睿。 AMD 从哪一款 cpu 开始支持 RDRAND ,笔者没找到详细的资料,但是可以肯定的是 zen 架构及以后的 cpu 型号都支持。 基本可以肯定现在的 x86 cpu 都支持这条指令。 AMD 如何实现 RDRAND ,笔者也没有找到具体的资料,但从一些英文的文章来看, AMD 的 RDRAND 速度似乎比英特尔的要慢不少。

从英特尔的文档来看 RDRAND 和 RDSEED 的区别

RDRAND 和 RDSEED 都有可能调用失败。 英特尔的文档里建议应用程序在紧密循环中尝试 10 次重试,以防 RDRAND 指令不返回随机数。 这个数字基于二项式概率论证:考虑到 DRNG 的设计余量,连续十次失败的几率非常小,实际上表明 CPU 问题更大。

使用内联汇编调用 RDRAND 的例子 只能运行在 x86-64 的 cpu 上,只能运行在 64 位系统上,只能用 gcc 编译

/*
先判断 cpu 是否支持,
    用 CPUID 指令来判断
再判断操作系统的位数,
    用宏定义 __x86_64 来判断
再判断编译器
    如果只是单纯地调用一次 RDRAND 指令,汇编语法用哪个都没关系的
    gcc 或 clang 用 ATT 汇编
    其它用 Intel 汇编
    用宏定义 _MSVCRT_ 来判断
*/
#include <stdio.h>
#include <stdint.h>

#define DRNG_NO_SUPPORT	0x0
#define DRNG_HAS_RDRAND	0x1
#define DRNG_HAS_RDSEED	0x2

typedef struct cpuid_struct {
    unsigned int eax;
    unsigned int ebx;
    unsigned int ecx;
    unsigned int edx;
} cpuid_t;

int get_drng_support();
void cpuid(cpuid_t *info, unsigned int leaf, unsigned int subleaf);
int rdrand16_step(uint16_t *rand);
int rdrand32_step(uint32_t *rand);
int rdrand64_step(uint64_t *rand);

int main()
{
    if (!(get_drng_support() & DRNG_HAS_RDRAND)) {
        printf("\ncurrent cpu is not support rdrand\n");
        return 1;
    }

    uint64_t rand = 0;
    unsigned int retries= 10;
    unsigned int count= 0;
    while (count <= retries) {
        if (!rdrand64_step(&rand)) {
            printf("\ncall rdrand fail\n");
            return 1;
        }
        ++count;
    }

    printf("\nRND64=0x%llx\n", rand);

    return 0;
}

int get_drng_support()
{
    static int drng_features= -1;
    if ( drng_features == -1 ) {
        drng_features= DRNG_NO_SUPPORT;
        cpuid_t info;
        cpuid(&info, 1, 0);
        if ((info.ecx & 0x40000000) == 0x40000000) {
            drng_features|= DRNG_HAS_RDRAND;
        }
        cpuid(&info, 7, 0);
        if ((info.ebx & 0x40000) == 0x40000) {
            drng_features|= DRNG_HAS_RDSEED;
        }
    }
    return drng_features;
}
void cpuid(cpuid_t *info, unsigned int leaf, unsigned int subleaf)
{
    asm volatile("cpuid"
    : "=a" (info->eax), "=b" (info->ebx), "=c" (info->ecx), "=d" (info->edx)
    : "a" (leaf), "c" (subleaf)
    );
}
int rdrand16_step(uint16_t *rand)
{
    unsigned char ok;

    asm volatile ("rdrand %0; setc %1"
        : "=r" (*rand), "=qm" (ok));

    return (int) ok;
}
int rdrand32_step(uint32_t *rand)
{
    unsigned char ok;

    asm volatile ("rdrand %0; setc %1"
        : "=r" (*rand), "=qm" (ok));

    return (int) ok;
}
int rdrand64_step(uint64_t *rand)
{
    unsigned char ok;

    asm volatile ("rdrand %0; setc %1"
        : "=r" (*rand), "=qm" (ok));

    return (int) ok;
}

如果编译器不支持 RDRAND ,可以直接用16进制的指令调用,例如这样

#include <stdio.h>
int main()
{
    long long unsigned int rnd64;
    asm volatile(
        ".byte 0x48,0x0f,0xc7,0xf0\n"
        "ret"
        :"=r"(rnd64):
    );
    printf("\nRND64=0x%llx\n", rnd64);
    return 0;
}

其它生成随机数的方式

可能是因为人们对 混沌系统 研究得不够深入才会觉得 大气噪声 或 电阻热噪声 是随机的

OpenSSL 的一般使用

OpenSSL是一个开放源代码的软件库包。这个包广泛被应用在互联网的网页服务器上。 其主要库是以C语言所写成,实现了基本的加密功能,实现了SSL与TLS协议。

以下命令均在 cygwin 或 linux 下运行

以下命令是在这个版本 OpenSSL 1.1.1g 21 Apr 2020 下的 OpenSSL 运行的

openssl version -a
openssl help
openssl 某个命令 --help
openssl ciphers -v
echo "123" | openssl dgst -sha256
openssl dgst -sha256 文件路径
echo "123" | openssl dgst -sha256 | awk '{print $2}'
openssl dgst -sha256 文件路径 | awk '{print $2}'
date +%s
date +%N
openssl rand -base64 32
# 输出随机数字,但无法确定数字的长度
openssl rand -base64 32 | tr -dc '0-9'
openssl rand -base64 64 | tr -dc '0-9'
# 生成16位随机数 有可能不足16位
openssl rand -base64 128 | tr -dc '0-9' | cut -c1-16
openssl enc -help
openssl enc -aes-256-cfb -e -in a.txt -a -out b.txt -pass pass:123
openssl enc -aes-256-cfb -d -in b.txt -a -out c.txt -pass pass:123
# -aes-256-cfb 使用的算法
# -e 加密
# -d 解密
# -in 输入的文件路径
# -out 输出文件的路径
# -a 把输出转换成 base64 加密时有这个参数,解密时也要有这个参数
# -pass 数入密码和输入密码的方式
#     pass
#     file
#     stdio
#     env
#     fd
openssl genrsa -out rsa_private_key.pem 4096
# 默认是 pem 格式的
# -out 指定生成文件的路径
# 最后的 4096 是生成密钥的长度
# 生成的密钥对是 pkcs1 格式的, openssl 有相应的命令转成 pkcs8 或 pkcs12 格式
openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
# -pubout 提取公钥
# -out 指定生成文件的路径
# -in 私钥路径
openssl rsautl -encrypt -in a.txt -inkey rsa_public_key.pem -pubin -out b.txt
# -out 加密后的文件路径
# -in 需要加密的文件路径
# -inkey 公钥路径
openssl rsautl -decrypt -in b.txt -inkey rsa_private_key.pem -out c.txt
# -out 解密后的文件路径
# -in 需要解密的文件路径
# -inkey 私钥路径
openssl dgst -sha256 -sign rsa_private_key.pem -keyform PEM -out sign.sha256 a.txt
# -sha256 哈希算法
# -sign 私钥路径
# -keyform 私钥的格式
# -out 签名生成的路径
# 最后的 a.txt 是需要生成签名的文件路径
openssl dgst -sha256 -verify rsa_public_key.pem -keyform PEM -signature sign.sha256 a.txt
openssl enc -base64 -e -in sign.sha256 -out sign.sha256.base64
openssl enc -base64 -d -in sign.sha256.base64 -out sign.sha2562
diff 文件1的路径 文件2的路径
openssl req -new \
    -key rsa_private_key.pem \
    -keyform PEM \
    -out myserver.csr
# -new 生成一个新的 csr 文件
# -key 私钥文件路径
# -keyform 私钥的格式
# -out 生成的 csr 文件路径
openssl req -text -in myserver.csr -noout -verify
# -in csr 文件路径
openssl x509 \
    -sha256 \
    -signkey rsa_private_key.pem \
    -in myserver.csr \
    -req -days 365 -out domain3.crt
# x509 生成 x509 格式的证书
# -sha256 证书采用的哈希算法
# -signkey 私钥路径
# -in csr 文件路径
# -days 证书有效天数
# -out 生成的证书路径
openssl req -newkey rsa:4096 -nodes -keyout rsa_private_key.pem -x509 -days 365 -out domain.crt
openssl req -newkey rsa:4096 -nodes -keyout rsa_private_key.pem -x509 -days 365 -out domain.crt -subj "/C=CN/ST=State/L=City/O=Ltd/OU=Section/CN=localhost"
openssl x509 -in domain.crt -noout -text
openssl x509 -in domain.crt -noout -serial
openssl x509 -in domain.crt -noout -dates

生成多个域名的证书

一般使用 OpenSSL 生成证书时都是 v1 版的,不带扩展属性。 多域名证书需要用到 v3 版的 extensions 的 Subject Alternative Name (SAN 主题替代名称)

  1. 寻找默认配置文件
find / -name openssl.cnf
  1. 复制一份默认配置文件
cp /usr/ssl/openssl.cnf openssl.cnf
  1. 编辑 openssl.cnf

    1. [ req ] 字段下加入 req_extensions = v3_req
    2. [ v3_req ] 字段下加入 subjectAltName = @alt_names
    3. 在配置文件的最后最后新建一个字段 [ alt_names ]
    4. 在 [ alt_names ] 里按以下格式写入多个域名
      [ alt_names ]
      DNS.1 = 3.example.com
      DNS.2 = 4.example.com
      
  2. 新建私钥

openssl genrsa -out rsa_private_key.pem 4096
  1. 生成 csr 文件
openssl req -new \
    -key rsa_private_key.pem \
    -keyform PEM \
    -config openssl.cnf \
    -out myserver.csr
  1. 生成数字证书
openssl x509 \
    -sha256 \
    -signkey rsa_private_key.pem \
    -in myserver2.csr \
    -extensions v3_req \
    -extfile openssl.cnf \
    -req -days 365 -out domain.crt

自建 CA

  1. 创建 CA 目录
mkdir -p ~/ssl/demoCA/{certs,newcerts,crl,private}
cd ~/ssl/demoCA
Touch index.txt
echo "01" > serial
  1. 寻找默认配置文件
find / -name openssl.cnf
  1. 复制一份默认配置文件
cp /usr/ssl/openssl.cnf ~/ssl/openssl.cnf
  1. 修改 openssl.cnf 文件

    • 把 [ CA_default ] 的 dir 修改成 ~/ssl/demoCA/ 的绝对路径,类似于这样
      [ CA_default ]
      dir		= /root/ssl/demoCA		# Where everything is kept
      
  2. 生成 CA 根证书及密钥

openssl req -new -x509 -newkey rsa:4096 -nodes -keyout cakey.key -out cacert.crt -config openssl.cnf -days 365
  1. 生成客户端私钥
openssl genrsa -out client.key 4096
  1. 用该客户端私钥生成证书签名请求
openssl req -new -key client.key -out client.csr -config openssl.cnf
  1. 使用 CA 根证书签发客户端证书
openssl ca -in client.csr -out client.crt -cert cacert.crt -keyfile cakey.key -config openssl.cnf

证书链合并

一些情况下,从 CA 那里申请到的 SSL 证书需要配置证书链,因为颁发的 CA 只是一个中间 CA 。 这个时候,需要把 CA 的证书和 SSL 证书都转换成 pem 格式。 然后新建一个文件,按照 最终实体证书 -> 中间证书 -> 根证书 这样的顺序,把证书的 pem 格式的内容复制进去,证书之间用一个空行隔开。 例如这样

-----BEGIN CERTIFICATE-----
这是 最终实体证书
------END CERTIFICATE------

-----BEGIN CERTIFICATE-----
这是 中间证书
------END CERTIFICATE------

-----BEGIN CERTIFICATE-----
这是 根证书
------END CERTIFICATE------

根证书大多数情况下都会内置在客户端,所以大多数情况下都只需要 最终实体证书 和 中间证书。 有时 中间证书 可能有多个,按照签发顺序排列就好,反正就是下面的证书颁发上面的证书。

有时还需要把私钥和证书合并成一个文件,一般是把私钥放在前面,证书放在后面,例如 这样

cat server.key server.crt > server.pem

其它命令

openssl s_time 用于测试 TSL 服务

openssl s_time -connect www.baidu.com:443 -www /index.html

openssl s_server 用于测试 TSL 客户端,例如浏览器对各个加密套件的支持情况

openssl s_server -accept 2009 -key rsa_private_key.pem -cert domain.crt -www -debug -msg
# -accept 监听的端口 -key 私钥路径 -crt 证书路径 -www http请求返回状态信息
# -WWW 或 -HTTP 参数,则可以启动一个简单的静态服务器
# 如果不设置 -www -WWW -HTTP ,客户端在终端输入任何字符,服务端都会响应同样的字符给客户端
# 可以用浏览器输测试,直接输入网址 https://127.0.0.1:2009
# 可以用 curl 测试 curl -k -i -v https://127.0.0.1:2009
# 可以用 openssl s_client 测试 openssl s_client -connect 127.0.0.1:2009 -showcerts

openssl s_client 用于测试 TSL 服务端

openssl smime 用于处理S/MIME邮件,它能加密、解密、签名和验证S/MIME消息

openssl ca ca命令是一个小型CA系统。它能签发证书请求和生成CRL。它维护一个已签发证书状态的文本数据库。

OpenSSH 的一般使用

OpenSSH (OpenBSD Secure Shell) 是 OpenBSD 的子项目。 OpenSSH 常常被误认以为与 OpenSSL 有关系,但实际上这两个项目有不同的目的,不同的发展团队,名称相近只是因为两者有同样的软件发展目标──提供开放源代码的加密通信软件。

程序主要包括了几个部分:

ssh rsh rlogin rexec 与 Telnet 的替代方案
scp rcp 的替代方案
sftp ftp 的替代方案
sshd SSH服务器
ssh-keygen 产生 RSA 或 ECDSA 密钥,用来认证用
ssh-agent ssh-add 帮助用户不需要每次都要输入密钥密码的工具

以下命令是在这个版本 OpenSSH_8.5p1, OpenSSL 1.1.1k 25 Mar 2021 下的 OpenSSH 运行的

sshd

ssh

ssh -o ServerAliveInterval=60 -o ServerAliveCountMax=30 -o TCPKeepAlive=yes 用户名@远程地址
# -o ServerAliveInterval 的意思是每 60 秒发送一次请求,用于保持连接的,不加这个参数也可以,但连接很容易就断开了
# -o TCPKeepAlive=yes 表示 TCP 保持连接不断开
# -o ServerAliveCountMax 表示连续 30 次服务端没有响应后,客户端就自动退出,如果有监控进程这类的,可以执行重新连接这类操作
ssh -o ServerAliveInterval=60 -i 密钥路径 用户名@远程地址

ssh 的端口转发

ssh 除了登录服务器,还有一大用途,就是作为加密通信的中介, 充当两台服务器之间的通信加密跳板, 使得原本不加密的通信变成加密通信。 这个功能称为端口转发(port forwarding),又称 SSH 隧道(tunnel)。

ssh 的端口转发有三种

ssh 文档 https://man.openbsd.org/ssh

https://wangdoc.com/ssh/port-forwarding

sftp

sftp -o ServerAliveInterval=60 用户名@远程地址
sftp -o ServerAliveInterval=60 -i 密钥路径 用户名@远程地址
# 上传文件
put 本地路径  远程路径
# 下载文件
get 路径路径  本地路径
# 查看服务器路径[默认用户根目录]
pwd
# 列出当前服务器路径下的文件
ls -al
# 更改服务器路径,就是普通的 cd 命令
cd
# 在服务器里新建文件夹
mkdir 文件夹名
# 删除服务器的文件
rm 文件名
# 删除服务器的文件夹
rm -r 文件夹名
# 查看本地路径
lpwd
# 列出本地路径下的文件
lls
# 更改本地路径,和 cd 一样只是前面多了个 l
lcd
# 在本地新建文件夹
lmkdir 文件夹名
# 执行其它本地命令,这里要注意本地的命令行环境
!本地命令

OpenSSH 和 OpenSSL

OpenSSH 是 SSH 协议的实现,实现过程中,需要用到密钥交换算法,对称/非对称加密算法,随机数算法,等密码学相关的算法。 早期版本的 OpenSSH 是通过调用 OpenSSL 的库来实现这些算法的。 2014年4月的心脏出血漏洞事件之后, OpenBSD 项目成员以 OpenSSL 1.0.1g 作为分支,创建一个名为 LibreSSL 的项目。 OpenSSH 会逐渐减少对 OpenSSL 的依赖。

GnuPG 的一般使用

1991 年,程序员 Phil Zimmermann 为了避开政府的监视,开发了加密软件 PGP (Pretty Good Privacy)。 但是,它是商业软不能自由使用。所以,自由软件基金会决定,开发一个 PGP 的替代品取名为 GnuPG (GNU Privacy Guard)。 OpenPGP (Pretty Good Privacy) 是一种加密标准。 GnuPG 是实现该标准的软件。

gpg 在需要用到私钥的地方都需要口令。 gpg 会对消息进行压缩。

gpg 的配置文件一般会保存在这个位置

linux ~/.gnupg
windows 系统盘/Users/用户名/.gnupg

加密和解密

签名和验签

一些命令参考

以下命令是在这个版本 gpg (GnuPG) 2.2.28 下的 GnuPG 运行的

gpg --help
gpg --gen-key
# 这里会要求输入用户名和邮箱,用户名和邮箱组合起来就是要用户ID了
# 例如 用户名是 username ,邮箱是 email@123.com ,那么用户ID 就是 username <email@123.com> ,还有就是在命令行里输入时要记得加上双引号
gpg --list-keys
gpg --armor --output public-key.txt --export [用户ID]
gpg --armor --output private-key.txt --export-secret-keys [用户ID]
gpg --import [密钥文件]
gpg --armor --recipient [用户ID] --output demo.en.txt --encrypt demo.txt
gpg --recipient [用户ID] --output demo.de.txt --decrypt demo.en.txt
gpg --armor --local-user [用户ID] --output demo.txt.sig --detach-sign demo.txt
gpg --armor --recipient [用户ID] --output demo.txt.sig --verify demo.txt
gpg --armor --local-user [发信者ID] --recipient [接收者ID] -output demo.txt.asc --sign --encrypt demo.txt
# 直接解密即可,解密的同时会验证签名
gpg --recipient [接收者ID] --output demo.de.txt --decrypt demo.txt.asc

GnuPG 和 email

很多邮件客户端都支持使用 GPG 来加密邮件。

只要导入发件人的私钥和收件人的公钥,就能发送加密的邮件。发件人的邮箱地址需要和私钥的邮箱地址对应,收件人的邮箱地址需要和公钥的邮箱地址对应。 使用加密后,会把整个邮件报文加密,包括主题,然后把密文保存在一个文件里,作为附件发送。 如果收件人的邮件客户端不支持直接解密邮件,可以把邮件的附件下载下来然后再用 GPG 解密。像这样 gpg --recipient [接收者ID] --output demo.de.txt --decrypt encrypted.asc

其实把明文加密后再把密文复制到邮件内容里发送也可以,但收件人的邮件客户端未必能解密,因为一般加密的邮件都是整个报文加密的,这种可能需要把邮件里的密文保存到单独的文件后再解密。

参考

https://zh.wikipedia.org/wiki/%E5%85%AC%E9%92%A5%E5%AF%86%E7%A0%81%E5%AD%A6%E6%A0%87%E5%87%86

https://zh.wikipedia.org/wiki/%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8

https://yeasy.gitbook.io/blockchain_guide/ 区块链技术指南 虽然是讲区块链的,但密码学部分很有参考价值

https://book.douban.com/subject/26822106/ 图解密码技术(第3版)

https://www.gnupg.org/howtos/zh/index.html GnuPG 袖珍 HOWTO (中文版)

https://docs.azure.cn/zh-cn/articles/azure-operations-guide/application-gateway/aog-application-gateway-howto-create-self-signed-cert-via-openssl

https://www.crypto101.io/ 密码学入门课程