#10 ARC(Authenticated Received Chain)
Email 信息安全 2023-02-08谷歌的邮件中看到 ARC-xxx 头,研究了一下,叫做 Authenticated Received Chain
好像中文资料还非常少,直译过来,可能应该叫做 可信邮件转发链。
解决的问题是邮件经过一些服务进行中转过程中,原有的安全措施 —— SPF,DKIM,DMARC —— 会失效的问题。
coding in a complicated world
谷歌的邮件中看到 ARC-xxx 头,研究了一下,叫做 Authenticated Received Chain
好像中文资料还非常少,直译过来,可能应该叫做 可信邮件转发链。
解决的问题是邮件经过一些服务进行中转过程中,原有的安全措施 —— SPF,DKIM,DMARC —— 会失效的问题。
HTTPS = HTTP + TLS
原来是 TCP -> HTTP
现在是 TCP -> TLS -> HTTP
提升了安全性的同时,降低了一些性能。
更重要的是,互联网基础设施层面开始淘汰 HTTP:
相关通信过程在 2021/01/08,了解 HTTPS 背后的原理 有描述。
损耗主要在哪些环节呢?
参考 HTTPS 的资源消耗,针对消耗点进行优化。
在 2012 年的 RFC 6797 中,HTTP 严格传输安全被定义为网络安全标准。 创建这个标准的主要目的,是为了。
HSTS(HTTP 严格传输安全)
RFC 6797。
作用是避免用户遭受使用 SSL stripping(剥离,HTTP 降级攻击) 的 中间人攻击(man-in-The-middle,MITM)
add_header Strict-Transport-Security "max-age=31536000";
HTTP/2 支持
双因素 / 多因素认证,就是指处理密码之外,再加一些辅助手段用来加强认证。
常见的 2FA 方式:
应该有人脸,指纹,声纹,虹膜等方式的组合(甚至在异形 4 中有吹气的方式),这些在我们的数字生活中这些认证方式多少都有一些体验吧。
事先给客户一张密码表,然后需要的时候要求客户按要求输入指定位置的数字用来验证身份。
原理应该和微信让用户指认哪个头像是自己的好友一样。
银行的 U 盾
一次性密码,One Time Password
TOTP 的全称是"基于时间的一次性密码"(Time-based One-time Password)。它是公认的可靠解决方案,已经写入国际标准 RFC6238。
不知道密码学上是什么原理,只知道可以每分钟生成一个 6 位密码。只要双方算法和密钥一致,就能实现认证。
软件的,谷歌身份验证器,微软身份验证器,这种。
硬件的,银行的动态口令,早些年网易的将军令,这种。
FIDO(Fast IDentity Online,线上快速身份验证)联盟是成立于2012年7月的行业协会。其宗旨是为解决强制认证设备的交互性和用户面临大量复杂的用户名和密码。
主要是一些巨头(Google,微软)和搞 2FA 硬件设备的厂商组件的联盟,目标是通过一个标准,让符合标准的技术能够到处适用,用起来更方便、更安全。
官方定义的愿景是 “减少世界对密码的依赖”。
协议:
说明:
阿里巴巴的 IFAA
腾讯的 TUSI
今天的《科技爱好者周刊(第 219 期):如何防止帐号被黑》中说:
上周有一起安全事件。两家著名的美国互联网公司----Twillo 和 Cloudflare----被攻击了,前者还被攻破了。
手段还是钓鱼,不止钓密码,也钓了 TOTP 验证码。所以阮一峰在文章中的意思是,物理密钥会更安全。
这么说确实有道理,其实我好多次都想买一个,就是太贵,两三百。
阮一峰还提到,有部分实践(Web Authentication)在尝试采用本地设备的认证手段作为第二因子,比如手机和笔记本上的指纹识别和人脸识别。
还是希望有厂商能够推出廉价一些的 Key。
Authenticator
密码就算是身份验证器。
加密密钥,比如 SSH 的公钥私钥(密钥对)
One-time Password, 缩写: OTP, 又叫做一次性密码
我们常用的的短信验证码就是一种非常方便快捷的动态密码形式。
早些年一些网络服务,比如谷歌、网易通行证等,可能会提供一个动态口令表,包含十几个密码,可以保存为图片,或者纯文本,上面的密码可以逐个使用,每个密码只能用一次。
简单来说就是根据密钥和计数器来生成一个一次性密码。
除了记住密钥之外,你还要记住这是第几次使用。
MAC[(19 × 8) + 4:(19 × 8) + 7] × 8
MAC[i × 8 + 1:i × 8 + (4 × 8) − 1]
Multi-factor authentication, 缩写: MFA
Two-factor authentication, 缩写: 2FA
Google 身份验证器是一款 TOTP 与 HOTP 的两步验证软件令牌,此软件用于 Google 的认证服务。此项服务所使用的算法已列于 RFC 6238 和 RFC 4226 中。
Google 身份验证器给予用户一个六位到八位的一次性密码用于进行登录 Google 或其他站点时的附加验证。其同样可以给第三方应用生成口令,例如密码管理员或网络硬盘。先前版本的 Google 身份验证器开放源代码,但之后的版本以专有软件的形式公开。
谷歌验证器基于 TOTP,但是更进一步简化,以约定代替了协商过程。
此外:
虽然不是很大的创新,而且这个软件验证器实现很简单,但是免费、开放(不需要做任何谷歌服务绑定),加上谷歌的强大影响力,这个软件验证器被很多系统采用。
最后,其他提供动态口令的应用都需要来兼容谷歌身份验证器。
PS: RedHat 开发并维护了开源的 FreeOTP 分支项目。
PS: 微软也有一个 Microsoft Authenticator,阿里云 APP 中有一个 虚拟MFA 功能,都是一个意思。
微软家的为自己提供 8 位密码,别人家的就 6 位,区别对待(虽然感觉好像也并没有什么影响)
阿里云 MFA 是需要手机 APP 登录进去之后才能使用的。
import base64
import hashlib
import hmac
import time
DEFAULT_INTERVAL = 30 # Google Authenticator: 30 秒
DEFAULT_HASH = hashlib.sha1
def get_hotp_token(secret: str, counter: int, hash_algorithm=DEFAULT_HASH, length=6):
padding_len = 8 - len(secret) % 8
if padding_len != 8:
assert 1 <= padding_len <= 7
secret += '=' * padding_len
key = base64.b32decode(secret, True)
message = (counter & 0xffffffffffffffff).to_bytes(8, 'big')
mac = hmac.new(key, message, hash_algorithm).digest()
loc = mac[-1] & 0x0F
token = (int.from_bytes(mac[loc:loc+4], 'big') & 0x7fffffff) % (10 ** length)
return token
def get_totp_token(secret, interval=DEFAULT_INTERVAL):
time_counter = int(time.time()) // interval
return get_hotp_token(secret, time_counter)
if __name__ == '__main__':
print('%06d' % get_totp_token(sys.argv[1]))
TLS, Transport Layer Security, 传输层安全性协议
SSL, Secure Sockets Layer, 安全套接层
协议 | 发布时间 | 状态 | 说明 |
---|---|---|---|
SSL 1.0 | 未公布 | 未公布 | |
SSL 2.0 | 1995 年 | 2011 年弃用 | |
SSL 3.0 | 1996 年 | 2015 年弃用 | |
TLS 1.0 | 1999 年 | 2021 年弃用 | RFC 2246 |
TLS 1.1 | 2006 年 | 2021 年弃用 | RFC 4346 |
TLS 1.2 | 2008 年 | RFC 5246 | |
TLS 1.3 | 2018 年 | RFC 8446 |
现在主流的是 TLS 1.2 和 TLS 1.3。
相比之下,TLS 1.3 安全性更好,性能也更好。
搜索一下 tls1.2 tls1.3 difference
或者 tls1.2 tls1.3 performance
就能看到很多相关比较。
SSL/TLS 的本质就是非对称加密。
安全传输层协议(TLS)用于在两个通信应用程序之间提供保密性和数据完整性。
该协议由两层组成:
- TLS 记录协议(TLS Record)
- TLS 握手协议(TLS Handshake)
握手协议(handshake protocol)
密钥规格变更协议(change cipher spec protocol)
应用数据协议(application data protocol)
警报协议(alert protocol)。
HTTP/2 规范并没有要求使用 TLS,但是主流浏览器开发商全部选择在 TLS 之上实现 HTTP/2。
微软宣布从 2022 年 9 月 13 日起, Internet Explorer 和 EdgeHTML( WebView 控件的呈现引擎) 将默认禁用 TLS 1.0 和 1.1。禁用 TLS 1.0 和 1.1 的工作早在 2020 年提出,但当时新冠病毒在全球爆发,大量开发者居家办公,微软只能推迟淘汰 TLS 1.0 和 1.1 的时间。
注意:不是弃用,仍然可以通过配置打开相关支持。
SMTP 会话中,发行方会通过 MAIL FROM
命令告诉收信方,这封邮件是谁发出的。
但是,由于邮件系统的设计上没有考虑如何对垃圾邮件进行有效防范,收信方需要一个机制来判断这封邮件的来源是否可靠。因此,人们提出了 SPF 方案:
MAIL FROM
地址之后,使用发信方 IP 地址和发信域 DNS 中记录的 IP 地址做一个比对。如果匹配,说明来源正确,否则,这封邮件可以被视为垃圾邮件,直接返回投递失败(硬退)。该方案 2006 年提交到 Network Working Group 成为草案,然后 2014 年成为建议标准。
注意:SPF 只是验证了邮件确实是从发信域所在网络投递出来的,但是不能防止邮件伪造和欺诈。应该结合其他技术,如 DKIM(DomainKeys Identified Mail)和 DMARC(Domain-based Message Authentication, Reporting, and Conformance),一起增强电子邮件的安全性。
dig +short txt qq.com
"v=spf1 include:spf.mail.qq.com -all"
dig +short txt spf.mail.qq.com
"v=spf1 include:qq-a.mail.qq.com include:qq-b.mail.qq.com include:qq-c.mail.qq.com include:biz-a.mail.qq.com include:biz-b.mail.qq.com include:biz-c.mail.qq.com include:biz-d.mail.qq.com -all"
dig +short txt qq-{a..c}.mail.qq.com biz-{a..d}.mail.qq.com
"v=spf1 ip4:101.226.139.0/25 ip4:101.91.43.0/25 ip4:101.91.44.128/25 ip4:112.64.237.128/25 ip4:116.128.173.0/25 ip4:121.51.40.128/25 ip4:121.51.6.0/25 ip4:162.62.52.214 ip4:162.62.55.67 ip4:162.62.57.0/24 ip4:162.62.58.211 ip4:162.62.58.216 -all"
"v=spf1 ip4:162.62.58.69 ip4:162.62.63.194 ip4:180.163.24.128/25 ip4:183.2.187.0/25 ip4:203.205.221.128/25 ip4:203.205.251.0/25 ip4:210.51.43.0/25 ip4:58.246.222.128/25 ip4:58.250.143.128/25 ip4:61.241.55.128/25 -all"
"v=spf1 ip4:113.108.92.0/25 ip4:121.14.77.0/25 ip4:81.69.217.16/28 ip4:54.164.151.162 -all"
"v=spf1 ip4:114.132.122.39 ip4:114.132.123.192 ip4:114.132.124.171 ip4:114.132.125.233 ip4:114.132.197.227 ip4:114.132.224.180 ip4:114.132.233.22 ip4:114.132.58.0/24 ip4:43.155.65.254 ip4:114.132.62.0/24 ip4:106.55.200.77 -all"
"v=spf1 ip4:114.132.63.24 ip4:114.132.64.0/26 ip4:114.132.65.219 ip4:43.155.67.158 ip4:114.132.67.179 ip4:114.132.73.137 ip4:114.132.74.132 ip4:43.154.209.5 ip4:43.154.197.177 ip4:43.154.155.102 ip4:43.155.80.173 ip4:43.154.221.58 -all"
"v=spf1 ip4:54.204.34.129 ip4:54.204.34.130 ip4:54.243.244.52 ip4:52.205.10.60 ip4:35.173.142.173 ip4:54.207.22.56 ip4:54.207.19.206 ip4:54.254.200.92 ip4:54.254.200.128 ip4:54.92.39.34 ip4:54.206.16.166 ip4:54.206.34.216 ip4:114.132.75.215 -all"
"v=spf1 ip4:52.59.177.22 ip4:18.194.254.142 ip4:18.132.163.193 ip4:18.169.211.239 ip4:13.245.186.79 ip4:13.245.218.24 ip4:15.184.224.54 ip4:15.184.82.18 ip4:114.132.76.87 ip4:114.132.77.159 ip4:114.132.78.196 ip4:114.132.79.153 ip4:43.154.54.12 -all"
"v=spf1":指定 SPF 记录的版本,目前只有 SPFv1 版本。
机制(Mechanisms):SPF 使用机制来指定哪些服务器被授权发送邮件。常用的机制有:
"all":定义了所有情况下的默认结果。可以是 "+all"(通过验证)或 "-all"(不通过验证)。例如,"-all" 表示除非匹配其他通过机制,否则所有情况都不通过验证,即严格拒绝伪造邮件。
"include":允许引用其他域名的 SPF 记录。例如:"include:example.com" 表示允许按照 example.com 的 SPF 记录来验证。
修饰符(Modifiers):修饰符可以在机制之后指定,用于调整验证的结果。常用的修饰符有:
"+":显示通过验证。
SPF 记录示例:
v=spf1 ip4:192.0.2.1 a mx ~all
以上示例表示允许 IP 地址为 192.0.2.1 的服务器、A 记录和 MX 记录指定的主机发送邮件,但验证结果为软失败,邮件可能会被接受,但标记为不可信。
IANA 设计了一种专门的 DNS 类型用来记录 SPF 信息,但是采用率非常低。
可以忽略,继续使用 TXT 类型来保存 SPF 信息。
DomainKeys Identified Mail,域名密钥识别邮件
作用是使用非对称加密(公钥 + 私钥)对邮件内容进行签名,防止伪造和篡改。
DKIM-Signature: v=1; a=rsa-sha256; d=example.net; s=brisbane;
c=relaxed/simple; q=dns/txt; i=foo@eng.example.net;
t=1117574938; x=1118006938; l=200;
h=from:to:subject:date:keywords:keywords;
z=From:foo@eng.example.net|To:joe@example.com|
Subject:demo=20run|Date:July=205,=202005=203:44:08=20PM=20-0700;
bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;
b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZ
VoG4ZHRNiYzR
Tag | Required | Meanning |
---|---|---|
v | ✓ | version |
a | ✓ | signing algorithm |
d | ✓ | Signing Domain Identifier (SDID) |
s | ✓ | selector |
c | - | canonicalization algorithm(s) for header and body |
q | - | default query method |
i | - | Agent or User Identifier (AUID) |
t | recommended | signature timestamp |
x | recommended | expire time |
l | - | body length |
h | ✓ | header fields - list of those that have been signed |
z | - | header fields - copy of selected header fields and values |
bh | ✓ | body hash |
b | ✓ | signature of headers and body |
邮件中的 DKIM-Signature 头:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=mail.instagram.com;
s=s1024-2013-q3; t=1674163477;
bh=6RXa/HYJQNKpB5PIGtLn7v1NE/4T5FaqxBLWNHVRZu8=;
h=Date:To:Subject:From:MIME-Version:Content-Type;
b=VAY3x16QtXeH1rQxu6eEbzhfgZl69m1sG9XzN3ym4FbWiMg+K+IfMGF4yszGYk8yO
YXAAJZuQfG45pjthISDDSwhhBK0WGgufQ8ofnzhNUN9WT/okEATC+JfzksS9w2Ts4V
ALa/4HHXnikQV5AFNJJNJIvWMN/fJ5c49nLkW024=
域名中的 DKIM 公钥:
dig TXT +short s1024-2013-q3._domainkey.mail.instagram.com
"k=rsa; t=s; h=sha256; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7twdVo+BW8Pv2poU5129KYmE6npHdxUU8fktUKTE9TNovCvLy5LVjYc3TQcUFjOH" "VaZ89ZCjmpAcrA2QnTEKZ/2QWV56gn6bWdFW4SFxnQdHjguBZQykfKe5KTxy2a/OxuA0x2dHfdnYfw7RVzr4uednpKcWJy4Rl3gM6XB1zDwIDAQAB"
# 方法一:使用 OpenDKIM
sudo apt install -y opendkim-tools
opendkim-genkey --help
opendkim-genkey -D . -d markjour.com -s s2020
# 方法二:使用 OpenSSL
openssl genrsa -out dkim-markjour-s2020.pem 2048
openssl rsa -in dkim-markjour-s2020.pem -pubout -outform der 2>/dev/null | openssl base64 -A > dkim-markjour-s2020.pub
s2020._domainkey.markjour.com
"v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjnfmXLuyBt0Tus/Bdr87GpLqcRCZX7UC61OiPZ1Y3MG42qBcdEAMJfu7qop7KOLL8cywTRxiX39ehmf0ZovAXH1KkijiX/16tkI3cO9T6KS4vyr0Ip3fsGgNgjn5rH3M5AZAmbym6DIzYrtpTiAKgLYmFLALd9SLi/OhFIltWK+QJhaJgcuWUXCzlry01Fdsv1qj28WdZ6PQbQrSffc1qzkvOEOlmZXwWjQfg5X4E3DR4WKenC6f5WdcJeXk4pUeBOdQDoEM+4uCk4S6cN3OuYEQbvVmfQ5RAlCODccx7lJemWZZnlIf+03FppUEEMENZ8tu3iixD24m2q9wDLDL5QIDAQAB
v
版本,只有一种选择:DKIM1,必须放最前面,可以忽略h
哈希算法,sha1 或者 sha256k
密钥类型,只有一种选择:rsa,可以忽略n
注释,可以忽略p
公钥(ASN.1 DER-encoded + Base64)s
服务类型,默认 *
,表示所有服务,可以忽略t
逗号隔开的标记参考:
headers = {'aaa': '123', 'bbb': '456', 'ccc': '789'}
mail = """
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=qq.com; s=s0907;
t=1331524836; bh=Pqr4lbxMcef/3IqsXx/edT0iwPe18N7n8qKmQSnLio8=;
h=X-QQ-SSF:X-QQ-Spam:X-QQ-BUSINESS-ORIGIN:X-Originating-IP:
X-QQ-STYLE:X-QQ-mid:From:To:Sender:Subject:Mime-Version:
Content-Type:Content-Transfer-Encoding:Date:X-Priority:Message-ID:
X-QQ-MIME:X-Mailer:X-QQ-Mailer:X-QQ-ReplyHash;
b=hF3hXt429Mp9WUJx9wQQYYk32EABCQST/OmV+dI+vJ/XIidVkc6fsh8l/vBz/optb
MDp0XIupHHkUozz6jwMryhHd/ZNjLNtBBAIOgl1wH7R016x8uTtDQink5uIPH+5
X-QQ-SSF: 0000000000010060
X-QQ-Spam: true
X-QQ-BUSINESS-ORIGIN: 2
X-Originating-IP: 61.151.148.196
X-QQ-STYLE:
X-QQ-mid: bizmail6t1331524835t2734595
From: "=?gb18030?B?wffE6g==?=" <thinkphp@qq.com>
To: "=?gb18030?B?uvqwug==?=" <ninedoors@126.com>
Sender: liuchen@topthink.net
Subject: =?gb18030?B?UmU60rvOu1RQZXK1xNLJu/M=?=
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_NextPart_4F5D74E3_DF6406C0_249C5176"
Content-Transfer-Encoding: 8Bit
Date: Mon, 12 Mar 2012 12:00:35 +0800
X-Priority: 3
Message-ID: <tencent_519100472CAA258E750FA58D@qq.com>
X-QQ-MIME: TCMime 1.0 by Tencent
X-Mailer: QQMail 2.x
X-QQ-Mailer: QQMail 2.x
X-QQ-ReplyHash: 3949397671
""".strip('\n')
https://docs.djangoproject.com/en/2.0/topics/auth/passwords/
在 Django 中,密码哈希存储在 auth_user 表中的 password 字段中。该字段的值包含了算法名称、迭代次数、盐值和哈希值,它们都是使用特定的格式进行编码的。例如,一个密码哈希的值可能如下所示:
pbkdf2_sha256$150000$V7v0fjbhMIhq$Gd/0XuOqK3ib6NBNGVwIKGXcFTUiNbTzdTNN8RiW24E=
用美元符号切割,得到 4 部分:
pbkdf2_sha256
算法名称150000
迭代次数V7v0fjbhMIhq
盐Gd/0XuOqK3ib6NBNGVwIKGXcFTUiNbTzdTNN8RiW24E=
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
]
默认使用的是 PBKDF2PasswordHasher
:
PBKDF2 是基于密钥的密码派生函数,它使用一个伪随机函数(PRF)和一个盐值来从给定密码派生出一个密钥。
PBKDF2 算法的核心是迭代的使用 PRF 函数,将每一次迭代的输出与前一次迭代的输出进行异或,生成最终的派生密钥。
大概逻辑如下:
import hashlib
import binascii
import os
class PBKDF2PasswordHasher:
algorithm = 'pbkdf2_sha256'
iterations = 100000
def salt(self):
return binascii.hexlify(os.urandom(16)).decode()
def encode(self, password, salt):
dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), self.iterations)
return '%d$%s$%s' % (self.iterations, salt, binascii.hexlify(dk).decode())
def verify(self, password, encoded):
iterations, salt, dk = encoded.split('$')
iterations = int(iterations)
hashed_password = self.encode(password, salt)
return hashed_password == encoded
from django.contrib.auth.hashers import make_password
password = '123456'
hashed_password = make_password(password)
from django.contrib.auth.hashers import check_password
is_correct_password = check_password(password, hashed_password)