Luke a Pro

Luke Sun

Developer & Marketer

🇺🇦
EN||

數位簽章:證明誰發送了訊息

| , 10 minutes reading.

1. 為什麼要關心這個問題?

你下載一個軟體更新。你怎麼知道它真的來自供應商而不是惡意軟體?

你收到一封來自銀行的郵件。你怎麼知道它真的來自你的銀行?

你電子簽署一份合約。法院怎麼知道你真的同意了?

數位簽章解決了這些問題。它們是軟體安全、安全通訊和法律電子文件的基礎。

2. 定義

數位簽章 是一種密碼學方案,它證明:

  1. 身分驗證:訊息是由聲稱的發送者建立的
  2. 完整性:訊息自簽章以來沒有被修改
  3. 不可否認性:發送者不能否認簽署了該訊息
實體簽名 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 <dev@example.com>"

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': 'user@example.com'
})

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. 本章小結

三點要記住:

  1. 數位簽章證明真實性和完整性。 它們保證誰簽章以及內容沒有被修改,但不加密也不證明現實世界的身分。

  2. 新專案使用 Ed25519。 它快速、安全且難以誤用。為了相容性回退到 ECDSA P-256,只有必需時才用 RSA。

  3. 簽章需要上下文。 有效的簽章只證明某人簽了某些東西——你需要憑證(PKI)來知道那個人是誰。

12. 下一步

我們現在可以驗證訊息來自特定私鑰的持有者。但我們如何知道那個金鑰屬於他們聲稱的那個人?

在下一篇文章中:憑證和 PKI——我們如何在網際網路上建立信任鏈並證明現實世界的身分。