這也是靠新工作機開啟的桃花源,RAM 夠大 SSD 夠快跑 Android 模擬器終於不再卡頓到氣血攻心,封印解除後我寫了第一支 MAUI App 程式 - QR Code 識別:

QR Code 掃瞄是我一直想玩的功能。像有些平台提供行動條碼登入,用手機 App 掃一下,不用敲帳號密碼就能登入,我覺得超酷,興起「大丈夫應如是」的想法:

有辦法自己寫 App 讀 QR Code,一切就有解了!

我用 Android 手機,要寫程式給自己用,就先研究 Android 吧。

要講究安全,用 Android 硬體(Android Keystore System)保存非對稱金鑰對動態產生內容做數位簽章,應是最安全可靠的做法。在 MAUI 可引用 Xamarin Android SDK, Android.Security.KeystoreJava.Security 有相關型別可以產生 RSA 金鑰存在 AndroidKeyStore、匯出 X509 憑證(包含公開金鑰)、並使用私密金鑰對一段資料進行數位簽名。

我用以下程式產生一對金鑰,匯出 X509 憑證(PEM 格式),對字串 "Hello World!" 做 SHA256withRSA 數位簽章,結果為 byte[32] 轉為 Base64 字串。(延伸閱讀:常见签名算法之SHA256withRSA)

var keyStore = KeyStore.GetInstance("AndroidKeyStore")!;
keyStore.Load(null);
//if (keyStore.ContainsAlias(KeyName)) keyStore.DeleteEntry(KeyName);
if (!keyStore.ContainsAlias(KeyName))
{
    var keyPairGenerator = KeyPairGenerator.GetInstance(KeyProperties.KeyAlgorithmRsa, "AndroidKeyStore")!;
    keyPairGenerator.Initialize(new KeyGenParameterSpec.Builder(KeyName, KeyStorePurpose.Sign)
        // ToJavaDate() 為自訂擴充方法將 DateTime 轉為 Java.Util.Date
        .SetCertificateNotBefore(new DateTime(2023, 1, 1).ToJavaDate())
        .SetCertificateNotAfter(new DateTime(2100, 12, 31).ToJavaDate())
        .SetCertificateSubject(new Javax.Security.Auth.X500.X500Principal("CN=Jeffrey"))
        .SetDigests(KeyProperties.DigestSha256)
        .SetSignaturePaddings(KeyProperties.SignaturePaddingRsaPkcs1)
        .SetKeySize(2048)
        .Build());
    keyPairGenerator.GenerateKeyPair();
}
var pkEntry = (keyStore.GetEntry(KeyName, null) as KeyStore.PrivateKeyEntry)!;
var cert = pkEntry.Certificate;
IEnumerable<string> wrapAtCol80(string s)
{
    for (var i = 0; i < s.Length; i += 80)
    {
        yield return s.Substring(i, Math.Min(80, s.Length - i));
    }
}
Debug.WriteLine("-----BEGIN CERTIFICATE-----\n" +
                string.Join("\n", wrapAtCol80(Convert.ToBase64String(cert.GetEncoded()))) +
                "\n-----END CERTIFICATE-----");
var sig = Signature.GetInstance("SHA256withRSA");
sig.InitSign(pkEntry.PrivateKey);
sig.Update(System.Text.Encoding.UTF8.GetBytes("Hello World!"));
byte[] digSign = sig.Sign();
var sigBase64 = Convert.ToBase64String(digSign);
Debug.WriteLine(sigBase64);

產生憑證內容如下:

-----BEGIN CERTIFICATE-----
MIICnzCCAYegAwIBAgIBATANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdKZWZmcmV5MCAXDTIzMDEw
MTAwMDAwMFoYDzIxMDAxMjMxMDAwMDAwWjASMRAwDgYDVQQDEwdKZWZmcmV5MIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEAuAtDjI/8rEva5SYVd4KkJB/WD8ae9Hp95vgeI6MFG0whpAWXRDYId1Wi
b7wxSRy1eBfbv0NIIAcnJKPOjVUZ9/1DKnFGcrmI3HkKP2+HzfdKdZ0Kn2aUcdoNThfcdVxFgczwA672
XdLW44gn6fQ/KySFe2Xrwq50oSiTAxonO4HXGkKlNcPsTnIsxqtPtDpw2Y1dmHpKWsotfuaCY1Z7804H
bUbzWjHPoWOazPBWyA1l8hErfMp7etbeM+uzeaIWBhH+tR0IaMtxg2I1WgQQwMLwlDrRCgwVRVA7Kwnl
Ur1e3hseXQHBErKXI2Gox8qEUaUDM2T4USicFx6+XBGmJwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCm
1/vihJqBb+o5eJkHNZXfNF09uI3MCfQNbdGmk09as7eng1ltveP2wWKa099KkE8/TXE9doPZMzgK4F1j
ZPutAYmHcJA5NuvJQge0nLRZ3VBLcjxHg3If9rMFim+up5srTqIr3Tdr3u/bZCUFHPSQ+d0i5hS+7fjk
6xDhQ/7vvdy6WjI2uaAuJ36i5Tj7nsnQ3zJ5qJh8WD53PSl5FuctFzjpb8EVfuYB3faEACBGsyv2B0DG
z+ISsYUit1zPxyUC6AG92CkE9ZRTqTg3k0IeHL6y8n7AkCzu3DSaVxtrfj8yu1hT26C5bOtiIZIvcypI
cP+Jl3eXoyTmxH8lfe+a
-----END CERTIFICATE-----

簽章內容如下:

OCC3H1CoHsoXcMyHwyBfr3LKU7H6LA5ayGBMXXvOdqLLWxKEwv0TDnkLk7+QzfhIXpkibNcZXpKxYkZnw8N/Qnpze8rJB4NO1/N8DhJlFslql5f2by1fY7bW2JJxkBSysuH+oMwGq8CMoWeV9+bx9iqbv7T3zygcLQdqfcU2TTBx3wPjxGt50xqPcuNlmAgr+9wehzSxOKRqqy9UczbMsO2wBp7mFp8lVA0AInUbwRdauy6c39LtNxlyo8k0KnWFOAmzC162tHDClTEx4dNKdyy6jKlr6kZtYGVc1IDpiKXI9G+MCaht2LVFt239j56FOqLK9bQr08peU9ehaf12aw==

重點戲來了,如果 App 將上述憑證及簽章傳到網站,我要怎麼用 .NET 讀取憑證並用它驗證簽章真偽?

首先,我們將憑證 PEM 內容在 Windows 存成 .cer 檔,從檔案總管檢視,可看到這是張自簽憑證,簽發者及發給對象是都 App 程式指定的 Jeffrey,而有效期限一如預期是 2023/1/1 到 2100/12/31,稍後從 .NET 程式應該要能解析出同樣資訊。

在 .NET 要讀取及使用 X509 憑證主要是使用 X509CertificateX509Certificate2 物件,前者提供簽發對象、有效日期等資訊,並可由檔案讀取憑證;後者則可存取所有 V2/V3 屬性,包含公私鑰,並使用它進行加解密、簽章或驗證。

多說無益,直接看程式碼:

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

var cert = new X509Certificate("test.cer");
Console.WriteLine($"Subject: {cert.Subject}");
Console.WriteLine($"Not Before: {cert.GetEffectiveDateString()}");
Console.WriteLine($"Not After: {cert.GetExpirationDateString()}");

var content = "Hello World!";
var sigB64 = "OCC3H1CoHso...略...eU9ehaf12aw==";
var sig = Convert.FromBase64String(sigB64);

var rsa = new X509Certificate2(cert).GetRSAPublicKey()!;
var verified = rsa.VerifyData(
    System.Text.Encoding.UTF8.GetBytes(content), sig,
    HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
Console.WriteLine(verified ? "Verified" : "Failed");    

如此,我們成功地用 App 提供的 X509 憑證驗證該數位簽章確實由該憑證對映的私鑰所簽署:

完成這一步,便可設計一個機制,註冊階段記錄特定使用者手機 AndroidKeyStore 特定金鑰的 X509 憑證,之後驗證身分時傳送一段隨機內容要求 App 用該金鑰簽署,再用資料庫中的 X509 憑證驗證真偽。由於 AndroidKeyStore 內的金鑰無法被複製,需要該手機硬體產生數位簽章才能通過驗證,達到一定的安全管控。

突破了這個關卡,ASP.NET Core 行動條碼登入看來有譜了,應該會很有意思。

Example of verification of digital signature signed by Android app with .NET.


Comments

Be the first to post a comment

Post a comment