數位簽章:證明誰發送了訊息
Published: Sat Feb 01 2025 | Modified: Sat Feb 07 2026 , 10 minutes reading.
1. 為什麼要關心這個問題?
你下載一個軟體更新。你怎麼知道它真的來自供應商而不是惡意軟體?
你收到一封來自銀行的郵件。你怎麼知道它真的來自你的銀行?
你電子簽署一份合約。法院怎麼知道你真的同意了?
數位簽章解決了這些問題。它們是軟體安全、安全通訊和法律電子文件的基礎。
2. 定義
數位簽章 是一種密碼學方案,它證明:
- 身分驗證:訊息是由聲稱的發送者建立的
- 完整性:訊息自簽章以來沒有被修改
- 不可否認性:發送者不能否認簽署了該訊息
實體簽名 vs 數位簽章:
實體簽名:
- 可以透過複製來偽造
- 不能檢測文件修改
- 無論文件內容如何,看起來都一樣
數位簽章:
- 在數學上與簽章者的私鑰綁定
- 任何修改都會使簽章失效
- 每個文件的簽章都不同3. 數位簽章如何運作
基本過程
簽章:
┌─────────────────────────────────────────────────────────────┐
│ 1. 對訊息雜湊 hash = SHA256(message) │
│ 2. 簽章雜湊 signature = Sign(hash, privKey) │
│ 3. 發送訊息 + 簽章 │
└─────────────────────────────────────────────────────────────┘
驗證:
┌─────────────────────────────────────────────────────────────┐
│ 1. 對訊息雜湊 hash = SHA256(message) │
│ 2. 驗證簽章 Verify(signature, hash, pubKey) │
│ 3. 如果有效,訊息是真實且未被修改的 │
└─────────────────────────────────────────────────────────────┘為什麼要先雜湊?
為什麼不直接簽章訊息?
1. 效能:
- RSA 只能簽章約 256 位元組
- 簽章 1MB 檔案需要數千次操作
- 雜湊將任意大小減少到 32 位元組
2. 安全性:
- 簽章原始資料有數學弱點
- 雜湊提供額外的安全層
3. 標準化:
- 簽章演算法的固定大小輸入
- 無論訊息大小,行為一致4. 簽章演算法
RSA 簽章 (RSA-PSS)
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
# 產生金鑰
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
public_key = private_key.public_key()
message = b"Contract: I agree to pay $1000"
# 使用 PSS 填充簽章(推薦)
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# 驗證
try:
public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("簽章有效:訊息是真實的")
except Exception:
print("簽章無效:訊息可能是偽造或被修改的")ECDSA(橢圓曲線 DSA)
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
# 產生金鑰(比 RSA 小得多)
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
message = b"Transfer 1 BTC to address xyz"
# 簽章
signature = private_key.sign(message, ec.ECDSA(hashes.SHA256()))
# 驗證
try:
public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
print("有效的 ECDSA 簽章")
except Exception:
print("無效的簽章")EdDSA (Ed25519) - 現代選擇
from cryptography.hazmat.primitives.asymmetric import ed25519
# 產生金鑰
private_key = ed25519.Ed25519PrivateKey.generate()
public_key = private_key.public_key()
message = b"Authenticate this request"
# 簽章(演算法是內建的,不需要選擇)
signature = private_key.sign(message)
# 驗證
try:
public_key.verify(signature, message)
print("有效的 Ed25519 簽章")
except Exception:
print("無效的簽章")對比
演算法 | 金鑰大小 | 簽章大小 | 速度 | 安全性
------------+----------+----------+--------+----------
RSA-2048 | 256 B | 256 B | 慢 | 112 位元
RSA-3072 | 384 B | 384 B | 更慢 | 128 位元
ECDSA P-256 | 64 B | 64 B | 快 | 128 位元
Ed25519 | 32 B | 64 B | 最快 | 128 位元
推薦:新專案使用 Ed25519
備選:ECDSA P-256 用於相容性
遺留:需要時使用 RSA-2048+5. 簽章保證什麼(和不保證什麼)
保證什麼
✓ 身分驗證
「這是由私鑰 X 的持有者簽章的」
✓ 完整性
「訊息自簽章以來沒有改變」
✓ 不可否認性
「簽章者不能否認簽署了這則確切的訊息」不保證什麼
✗ 身分
「簽章者就是他們聲稱的那個人」
(你需要憑證來做到這點——下一篇文章)
✗ 機密性
「訊息是保密的」
(簽章不加密——訊息是公開的)
✗ 時間戳記
「這是在時間 X 簽章的」
(你需要可信時間戳記)
✗ 授權
「簽章者被授權簽署這個」
(商業邏輯,不是密碼學)6. 常見用例
程式碼簽章
為什麼重要:
- 證明軟體來自聲稱的發布者
- 檢測篡改(惡意軟體注入)
- 作業系統可以阻止未簽章/未知軟體
運作原理:
開發者 使用者
│ │
│ 1. 建立軟體 │
│ 2. 雜湊可執行檔 │
│ 3. 用私鑰簽章 │
│ (來自 CA) │
│ │
│──── signed.exe ────────>│
│ │
│ 4. 作業系統驗證簽章 │
│ 5. 檢查憑證鏈 │
│ 6. 如果有效則執行 │Git 提交簽章
# 設定簽章金鑰
git config --global user.signingkey YOUR_KEY_ID
git config --global commit.gpgsign true
# 簽章提交
git commit -S -m "Signed commit"
# 驗證提交
git log --show-signature
# 輸出:
# commit abc123
# gpg: Signature made Wed Feb 1 2025 10:00:00
# gpg: using RSA key ABCD1234...
# gpg: Good signature from "Developer Name <[email protected]>"JWT(JSON Web Tokens)
import jwt
import datetime
# 伺服器的私鑰
PRIVATE_KEY = """-----BEGIN EC PRIVATE KEY-----
...
-----END EC PRIVATE KEY-----"""
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----"""
# 建立簽章令牌
payload = {
'user_id': 123,
'role': 'admin',
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
token = jwt.encode(payload, PRIVATE_KEY, algorithm='ES256')
print(f"令牌: {token}")
# 驗證令牌
try:
decoded = jwt.decode(token, PUBLIC_KEY, algorithms=['ES256'])
print(f"有效令牌,使用者: {decoded['user_id']}")
except jwt.InvalidSignatureError:
print("令牌簽章無效 - 可能被篡改")
except jwt.ExpiredSignatureError:
print("令牌已過期")文件簽章 (PDF)
# 使用 pyHanko 進行 PDF 簽章(概念範例)
from pyhanko.sign import signers
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
# 載入 PDF
with open('contract.pdf', 'rb') as f:
w = IncrementalPdfFileWriter(f)
# 使用憑證簽章
signer = signers.SimpleSigner.load(
'signing_key.pem',
'signing_cert.pem',
key_passphrase=b'password'
)
# 套用簽章
signers.sign_pdf(
w,
signers.PdfSignatureMetadata(field_name='Signature1'),
signer=signer
)
with open('contract_signed.pdf', 'wb') as out:
w.write(out)7. 安全考量
絕不簽章攻擊者控制的資料
# 危險:簽章任意使用者輸入
def vulnerable_sign(user_data):
return private_key.sign(user_data) # 攻擊者控制內容!
# 安全:簽章你自己的結構化資料
def safe_sign(action, resource_id, timestamp):
# 你控制結構和內容
message = f"{action}:{resource_id}:{timestamp}".encode()
return private_key.sign(message)簽章可延展性
某些簽章方案是可延展的:
給定訊息 M 的有效簽章 S,
攻擊者可以建立 S',它對 M 也是有效的。
ECDSA 是可延展的:
(r, s) 和 (r, -s mod n) 對同一訊息都有效
影響:
- 通常不是問題
- 在某些協定中可能導致問題
- 比特幣曾因此有漏洞
防禦:
- 使用確定性簽章 (Ed25519)
- 規範化簽章(只使用低 s)金鑰管理
私鑰安全就是一切:
如果私鑰洩露:
- 攻擊者可以以你的身分簽章
- 所有過去的簽章仍然有效
- 必須撤銷並重新簽章所有內容
最佳實務:
- 高價值金鑰儲存在 HSM 中
- 使用密碼保護的金鑰檔案
- 定期輪換簽章金鑰
- 安全地儲存離線備份8. 時間戳記
問題
Alice 在 2025 年 2 月 1 日簽署了一份文件。
她的金鑰在 2025 年 3 月 1 日過期。
在 2025 年 4 月 1 日:
- 簽章在數學上仍然有效
- 但它是在過期之前還是之後建立的?
- Alice 可能在撤銷後簽章!可信時間戳記
1. Alice 建立簽章
2. Alice 將簽章發送到時間戳記機構 (TSA)
3. TSA 簽章:「我在時間 X 看到了這個簽章」
4. TSA 回傳時間戳記令牌
現在我們可以證明:
- 簽章在特定時間存在
- 金鑰在那個時間是有效的RFC 3161 時間戳記
# 概念範例 - 實際實作有所不同
import hashlib
import requests
def get_timestamp(signature_bytes):
# 建立時間戳記請求
digest = hashlib.sha256(signature_bytes).digest()
# 發送到 TSA
response = requests.post(
'http://timestamp.example.com/tsa',
data=create_timestamp_request(digest),
headers={'Content-Type': 'application/timestamp-query'}
)
return parse_timestamp_response(response.content)
# 時間戳記令牌證明簽章的建立時間9. 實務中的簽章驗證
完整範例
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
import json
import base64
import time
class SignedMessage:
def __init__(self, private_key=None):
if private_key:
self.private_key = private_key
self.public_key = private_key.public_key()
else:
self.private_key = ed25519.Ed25519PrivateKey.generate()
self.public_key = self.private_key.public_key()
def sign(self, data: dict) -> dict:
"""簽章帶後設資料的訊息"""
# 新增後設資料
message = {
'data': data,
'timestamp': int(time.time()),
'signer': self._get_key_id()
}
# 確定性序列化
message_bytes = json.dumps(message, sort_keys=True).encode()
# 簽章
signature = self.private_key.sign(message_bytes)
return {
'message': message,
'signature': base64.b64encode(signature).decode()
}
def verify(self, signed_message: dict, public_key) -> dict:
"""驗證簽章訊息"""
message = signed_message['message']
signature = base64.b64decode(signed_message['signature'])
# 重建被簽章的確切位元組
message_bytes = json.dumps(message, sort_keys=True).encode()
# 驗證簽章
public_key.verify(signature, message_bytes)
return message['data']
def _get_key_id(self) -> str:
"""取得公鑰的短識別碼"""
pub_bytes = self.public_key.public_bytes(
serialization.Encoding.Raw,
serialization.PublicFormat.Raw
)
return base64.b64encode(pub_bytes[:8]).decode()
# 使用
signer = SignedMessage()
# 簽章
signed = signer.sign({
'action': 'transfer',
'amount': 100,
'to': '[email protected]'
})
print("簽章訊息:")
print(json.dumps(signed, indent=2))
# 驗證
try:
data = signer.verify(signed, signer.public_key)
print(f"驗證成功!資料: {data}")
except Exception as e:
print(f"驗證失敗: {e}")10. 常見錯誤
| 錯誤 | 後果 | 正確做法 |
|---|---|---|
| 不驗證簽章 | 接受偽造訊息 | 信任前始終驗證 |
| 不雜湊直接簽章 | 效能和安全問題 | 使用標準簽章方案 |
| 使用 MD5/SHA1 | 易受碰撞攻擊 | 使用 SHA-256 或 SHA-3 |
| 在程式碼中儲存私鑰 | 金鑰洩露 | 使用 HSM、環境變數或金鑰保管庫 |
| 不檢查金鑰有效性 | 接受已撤銷金鑰的簽章 | 檢查憑證鏈 |
| 混淆加密和簽章 | 錯誤的安全屬性 | 它們解決不同問題 |
11. 本章小結
三點要記住:
數位簽章證明真實性和完整性。 它們保證誰簽章以及內容沒有被修改,但不加密也不證明現實世界的身分。
新專案使用 Ed25519。 它快速、安全且難以誤用。為了相容性回退到 ECDSA P-256,只有必需時才用 RSA。
簽章需要上下文。 有效的簽章只證明某人簽了某些東西——你需要憑證(PKI)來知道那個人是誰。
12. 下一步
我們現在可以驗證訊息來自特定私鑰的持有者。但我們如何知道那個金鑰屬於他們聲稱的那個人?
在下一篇文章中:憑證和 PKI——我們如何在網際網路上建立信任鏈並證明現實世界的身分。
