Mã hóa bất đối xứng và chữ ký số: RSA, ECC, Ed25519

4 câu hỏi~30 phútTừ khóa: public key, private key, RSA, ECC, ECDSA, Ed25519, RSA-OAEP, RSA-PSS, hybrid encryption, digital signature, RS256, ES256, key size

Tiếp đi: mã hóa bất đối xứng. shop-app sắp gửi webhook "order.shipped" cho 30 đối tác. Dùng HMAC thì phải đưa secret cho cả 30 bên. Có cách nào khác không?

Có: chữ ký số với cặp public/private key. shop-app giữ private key để ký; 30 đối tác chỉ cần public key để verify. Không ai trong số họ giả mạo được shop-app.

1. Bài toán

shop-app ── POST order.shipped ──> Partner 1
         ── POST order.shipped ──> Partner 2
         ...                       Partner 30

2. Vấn đề với HMAC ở đây

3. Giải pháp: cặp khóa bất đối xứng

Thao tácDùng key nàoMục đích
EncryptPublic key của người nhậnChỉ người nhận (có private key) đọc được
DecryptPrivate key của người nhận
SignPrivate key của người gửiChứng minh người gửi, không bị sửa
VerifyPublic key của người gửiAi cũng verify được

Hai họ thuật toán:

Asymmetric chậm hơn AES hàng nghìn lần và RSA chỉ mã hóa được vài trăm bytes. Thực tế dùng hybrid: asymmetric để trao đổi / bọc một AES key, AES-GCM cho dữ liệu. TLS làm đúng như vậy (bài TLS).

4. Code từng bước

Bước 1: Dependency: không cần, java.security.Signature có sẵn.

Bước 2: Sinh key và config

# Ed25519 private key (PKCS#8) và public key (X.509)
openssl genpkey -algorithm ed25519 -out webhook-signing.pem
openssl pkey -in webhook-signing.pem -pubout -out webhook-signing.pub.pem
# Public key gửi cho partner / công bố tại https://api.shop.example/.well-known/webhook-keys
shop:
  webhook:
    signing-key: ${WEBHOOK_SIGNING_KEY}   # nội dung PKCS#8 Base64 (bỏ dòng BEGIN/END)
    key-id: wh-2026-10

Bước 3: Code Java, ký ở shop-app:

@Component
public class WebhookSigner {
    private final PrivateKey key;
    private final String keyId;

    public WebhookSigner(@Value("${shop.webhook.signing-key}") String pkcs8B64,
                         @Value("${shop.webhook.key-id}") String keyId) throws Exception {
        this.key = KeyFactory.getInstance("Ed25519")
                .generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(pkcs8B64)));
        this.keyId = keyId;
    }

    public Map<String, String> sign(long ts, byte[] body) throws GeneralSecurityException {
        Signature s = Signature.getInstance("Ed25519");
        s.initSign(key);
        s.update((ts + ".").getBytes(StandardCharsets.UTF_8));
        s.update(body);
        return Map.of("X-Key-Id", keyId,
                      "X-Timestamp", String.valueOf(ts),
                      "X-Signature", Base64.getEncoder().encodeToString(s.sign()));
    }
}

Partner verify (cũng là Java):

PublicKey pub = KeyFactory.getInstance("Ed25519")
        .generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(pubB64)));
Signature v = Signature.getInstance("Ed25519");
v.initVerify(pub);
v.update((ts + ".").getBytes(StandardCharsets.UTF_8));
v.update(rawBody);
boolean ok = v.verify(Base64.getDecoder().decode(sigB64));
// + kiểm timestamp, idempotency như bài 13.2

Với RSA thay "Ed25519" bằng "RSASSA-PSS" (cần PSSParameterSpec) hoặc "SHA256withRSA"; ECDSA là "SHA256withECDSA".

Bước 4: Test bằng curl

printf '%s' "$TS.$BODY" > msg.bin
echo "$SIG" | base64 -d > sig.bin
openssl pkeyutl -verify -pubin -inkey webhook-signing.pub.pem -rawin -in msg.bin -sigfile sig.bin
# Signature Verified Successfully

5. Cách hoạt động

Ký:     message ──hash──> digest ──[private key]──> signature
Verify: message ──hash──> digest ─┐
        signature ──[public key]──┴─> khớp? true/false

Hybrid encryption (ví dụ partner gửi file nhạy cảm cho shop-app):
  partner: aesKey = random 256 bit
           ct      = AES-GCM(aesKey, file)
           wrapped = RSA-OAEP(shopPublicKey, aesKey)    gửi (wrapped, iv, ct)
  shop:    aesKey  = RSA-OAEP-decrypt(shopPrivateKey, wrapped)
           file    = AES-GCM-decrypt(aesKey, ct)

6. So sánh

Độ an toàn tương đươngSymmetricRSAECC
~112 bit-2048224
~128 bitAES-1283072256 (P-256, Ed25519)
~192 bitAES-1927680384
~256 bitAES-25615360521
JWT algLoạiKeyGhi chú
HS256HMACShared secret ≥ 256 bitMột service vừa phát vừa verify
RS256RSA PKCS#1 v1.5 + SHA-256RSA 2048+Phổ biến nhất, Keycloak mặc định; chữ ký 256 bytes
PS256RSA-PSSRSA 2048+Padding hiện đại hơn RS256
ES256ECDSA P-256EC 256 bitToken ngắn hơn (chữ ký 64 bytes), ký nhanh
EdDSAEd2551932 bytesNhanh, an toàn, hỗ trợ thư viện còn ít hơn

Cấu hình JWT trong Spring (JwtEncoder/Decoder, JWKS) xem bài JWT.

7. Khi nào nên dùng / không nên dùng

Mình hay nghe "chữ ký là mã hóa hash bằng private key". Vậy ký và mã hóa là một, chỉ đổi chiều key thôi đúng không?

Cách hiểu đó chỉ gần đúng với RSA trong sách giáo khoa, và dễ gây hiểu lầm:

Nên nói: chữ ký là một phép toán riêng dùng private key, ai có public key đều verify được. Mã hóa nhằm bí mật; chữ ký nhằm xác thực.

Mình tự ký JWT ES256 bằng Signature.getInstance("SHA256withECDSA") rồi ghép header.payload.signature. Thư viện bên partner báo invalid signature, dù key đúng. Sao vậy?

Lỗi định dạng chữ ký kinh điển. Java trả chữ ký ECDSA ở dạng DER (ASN.1, khoảng 70–72 bytes), còn JWS yêu cầu dạng raw R||S đúng 64 bytes cho ES256.

Java SHA256withECDSA:  30 45 02 20 <R 32B> 02 21 00 <S 32B>   (DER)
JWS ES256 cần:         <R 32B><S 32B>                         (raw, 64B)

Cách sửa:

Phỏng vấn hỏi về mã hóa bất đối xứng và chữ ký số thì trả lời sao?

Q1. Mã hóa đối xứng và bất đối xứng khác nhau thế nào, dùng khi nào?

🇻🇳 Tiếng Việt:

Đối xứng như AES dùng một key cho cả hai chiều, rất nhanh, nhưng phải chia sẻ key an toàn. Bất đối xứng như RSA, ECC có cặp public và private key, giải quyết việc trao đổi key và chữ ký số nhưng chậm và giới hạn kích thước dữ liệu. Thực tế dùng hybrid: bất đối xứng để thỏa thuận hoặc bọc AES key, AES-GCM cho dữ liệu, giống TLS.

🇬🇧 English:

Symmetric encryption such as AES uses one key for both directions and is very fast, but the key must be shared securely. Asymmetric crypto such as RSA or ECC uses a public/private key pair, which solves key exchange and enables digital signatures, but it is slow and limited in data size. In practice we use hybrid encryption: asymmetric to agree on or wrap an AES key, and AES-GCM for the data, just like TLS.

Q2. RS256 và HS256 cho JWT khác gì, chọn cái nào?

🇻🇳 Tiếng Việt:

HS256 là HMAC với shared secret, ai verify được thì cũng ký được, hợp khi một service vừa phát vừa kiểm token. RS256 hay ES256 ký bằng private key ở auth server, resource server chỉ cần public key qua JWKS nên không thể giả token. Với nhiều service hoặc dùng Keycloak, em chọn RS256 hoặc ES256.

🇬🇧 English:

HS256 is HMAC with a shared secret, so anyone who can verify can also sign; it fits when one service both issues and validates tokens. RS256 or ES256 signs with a private key on the auth server, and resource servers only need the public key from JWKS, so they cannot forge tokens. With many services or an identity provider like Keycloak, I choose RS256 or ES256.

Q3. RSA hay ECC, key size bao nhiêu?

🇻🇳 Tiếng Việt:

RSA tối thiểu 2048 bit, 3072 nếu cần dùng lâu dài. ECC đạt độ an toàn tương đương với key nhỏ hơn nhiều: P-256 hay Ed25519 tương đương RSA 3072, ký nhanh và chữ ký ngắn. Hệ thống mới em ưu tiên Ed25519 hoặc ES256, giữ RSA khi cần tương thích.

🇬🇧 English:

RSA should be at least 2048 bits, and 3072 for long-term use. ECC reaches the same strength with much smaller keys: P-256 or Ed25519 roughly match RSA-3072, with faster signing and shorter signatures. For new systems I prefer Ed25519 or ES256 and keep RSA where compatibility requires it.

Cách nhớ ngắn gọn


Tự kiểm tra

Để gửi dữ liệu bí mật cho partner, dùng public key của ai?

Public key của partner (người nhận). Chỉ partner có private key giải mã được.

Vì sao không mã hóa file 10 MB trực tiếp bằng RSA?

RSA chỉ mã hóa được dữ liệu nhỏ hơn kích thước key trừ padding, và rất chậm. Dùng hybrid: AES-GCM cho file, RSA-OAEP bọc AES key.

Chữ ký số cho thêm gì so với HMAC?

Người verify không ký được (tách quyền), và non-repudiation.

Ed25519 có thao tác encrypt không?

Không, chỉ ký và verify. Trao đổi key dùng X25519.

Java ký ECDSA ra định dạng gì, JWS cần định dạng gì?

Java mặc định DER; JWS cần raw R||S. Dùng SHA256withECDSAinP1363Format hoặc thư viện JOSE.