🔐Google HSM 的PKCS#1簽章 轉 PKCS#7簽章攻略 via C# | HSM | 銀行直連 | E bank | ERP

by | 3 月 26, 2025 | 程式 | 0 comments

Views: 16

這個我弄蠻很久,覺得坑很多,這邊前前後後大概花了有三週才弄好簽章轉換,把踩過的坑紀錄在這邊。

聲明

目前我還在處於跟銀行對接測試,這篇給需要的人參考就好

完整程式碼

請參考我Github這個專案。

環境

  1. Windows 11 專業版 23H2
  2. .Net 9
  3. Visual Studio 2022

前置作業

  1. Google HSM那邊產出對應的csr檔跟json檔,以及一些設定資訊
  2. 開發環境跟運作環境要把json裝入認證

本篇不會介紹HSM產csr的部分(這邊我沒權限,同事管理的)

PKCS#1 與 PKCS#7 概念教學

什麼是 PKCS#1?

Public-Key Cryptography Standards #1 描述如何使用 RSA 做簽章與加解密

核心內容

  • 定義了 RSA 的加密/簽章格式
  • 包含簽章 padding(PKCS#1 v1.5 與 PSS)

PKCS#1 簽章流程(以 SHA-256 為例):

  1. 對原始資料做 SHA-256 雜湊
  2. 使用 RSA 私鑰對雜湊結果簽章
  3. 結果是一個固定長度(如 256 bytes)的 PKCS#1 格式簽章

在 HSM、KMS 中,這就是你用 AsymmetricSign(digest) 得到的簽章

什麼是 PKCS#7?

全名:Public-Key Cryptography Standards #7(又稱 CMS, Cryptographic Message Syntax)

用途:封裝簽章資料與簽章人資訊的標準格式

常見用途

  • 電子發票簽章
  • PDF/XAdES/CAdES 簽章格式
  • 數位憑證的簽章驗證封包

PKCS#7 主要欄位(以 SignedData 為主):

欄位名稱說明
digestAlgorithms使用的雜湊演算法(如 SHA256)
encapContentInfo原始資料或空值(detached 模式)
certificates包含的 X.509 憑證
signerInfos簽章者資訊與簽章內容

PKCS#7 支援兩種簽章形式:

類型說明
❌ 無 Attributes對原始資料雜湊 → 簽章
✅ 有 SignedAttributes對 SignedAttributes 的 DER 結構雜湊 → 簽章(強驗章

PKCS#1 與 PKCS#7 的關係

名稱角色資料類型
PKCS#1簽章本身(RAW)byte[] 長度固定(與金鑰長度相同)
PKCS#7簽章封裝格式ASN.1 DER 結構,可 base64、可嵌憑證

Google連線認證Json設定(Windows 環境)

要設定環境變數

透過PowerShell安裝

安裝方法 PowerShell

setx GOOGLE_APPLICATION_CREDENTIALS "C:\......\googlehsm.json"

驗證方式1:Google Cloud SDK驗證

使用Google Cloud SDK(推薦)

gcloud auth application-default print-access-token

這個是測試連線,如果連線成功會吐出一串英文token

驗證方式2: 使用Power Shell

echo $env:GOOGLE_APPLICATION_CREDENTIALS

這個只是印出環境變數

驗證方式3: C#

Console.WriteLine("取得GOOGLE_APPLICATION_CREDENTIALS");
string credentialPath = Environment.GetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS");
Console.WriteLine($"GOOGLE_APPLICATION_CREDENTIALS: {credentialPath}");

這個也只是印出環境變數

C# 套件(Nuget)

基本上C#對這塊還不是很完整,需要透過第三方套件

  1. Google.Cloud.Kms.V1 3.6
  2. Portable.BouncyCastle 1.9
  3. System.Security.Cryptography.Pkcs 9.0.3

其中關鍵是產出PKCS#7的時候要透過 Portable.BouncyCastle 這個套件,我試過其它的BouncyCastle 不同版本會缺東缺西。

參數定義

private static readonly string _projectId = "...";
private static readonly string _locationId = "...";
private static readonly string _keyRingId = "....";
private static readonly string _keyId = "...";

這幾個參數要先定義好,內容是產生Google HSM金鑰時候的參數

Google PKCS#1 簽章

  1. 使用 Google HSM 產生 PKCS#1 簽章
  2. 驗證 PKCS#1 簽章

使用 Google HSM 產生 PKCS#1 簽章 透過C#

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Google.Cloud.Kms.V1;
using Google.Protobuf;

/// <summary>
/// 從Google Cloud KMS取得PKCS#1簽章
/// </summary>
/// <param name="projectId"></param>
/// <param name="locationId"></param>
/// <param name="keyRingId"></param>
/// <param name="keyId"></param>
/// <param name="keyVersion"></param>
/// <param name="data"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static byte[] SignDataWithPKCS1(
    string projectId,
    string locationId,
    string keyRingId,
    string keyId,
    string keyVersion, // 使用第一個金鑰版本
    byte[] data = null)
{
    if (data == null)
    {
        throw new ArgumentNullException(nameof(data));
    }

    // 建立 KMS 用戶端
    KeyManagementServiceClient client = KeyManagementServiceClient.Create();

    // 建立金鑰版本名稱 (非對稱簽章必須指定金鑰版本)
    CryptoKeyVersionName keyVersionName = new CryptoKeyVersionName(projectId, locationId, keyRingId, keyId, keyVersion);

    // 計算資料的 SHA-256 雜湊值
    using (var sha256 = SHA256.Create())
    {
        byte[] hash = sha256.ComputeHash(data);
        // 建立 Digest 物件(如果你使用的簽章演算法與雜湊演算法相符)
        Digest digest = new Digest { Sha256 = ByteString.CopyFrom(hash) };

        // 呼叫 AsymmetricSign API 進行簽章
        AsymmetricSignResponse response = client.AsymmetricSign(keyVersionName, digest);

        // 回傳簽章結果,這就是 PKCS#1 格式的簽章
        return response.Signature.ToByteArray();
    }
}

驗證簽章

/// <summary>
/// 取得指定金鑰版本的公鑰,並驗證簽章
/// </summary>
/// <param name="projectId">專案ID</param>
/// <param name="locationId">區域ID</param>
/// <param name="keyRingId">金鑰環ID</param>
/// <param name="keyId">金鑰ID</param>
/// <param name="keyVersion">金鑰版本</param>
/// <param name="data">原始資料 (byte[])</param>
/// <param name="signature">由 KMS 產生的 PKCS#1 簽章 (byte[])</param>
/// <returns>若簽章驗證成功回傳 true,否則 false</returns>
public static bool VerifySignature(
    string projectId,
    string locationId,
    string keyRingId,
    string keyId,
    string keyVersion,
    byte[] data,
    byte[] signature)
{
    // 建立 KMS 用戶端
    KeyManagementServiceClient client = KeyManagementServiceClient.Create();

    // 建立金鑰版本名稱
    CryptoKeyVersionName keyVersionName = new CryptoKeyVersionName(projectId, locationId, keyRingId, keyId, keyVersion);

    // 取得公鑰 (PEM 格式)
    var publicKeyResponse = client.GetPublicKey(keyVersionName);
    string publicKeyPem = publicKeyResponse.Pem;
    if (string.IsNullOrEmpty(publicKeyPem))
    {
        throw new Exception("未取得公鑰 PEM 資料");
    }

    // 將 PEM 轉換為 RSA 公鑰
    using (RSA rsa = RSA.Create())
    {
        try
        {
            // .NET 5 及以上支援 ImportFromPem,將 PEM 轉成 RSA 公鑰
            rsa.ImportFromPem(publicKeyPem.ToCharArray());
        }
        catch (Exception ex)
        {
            throw new Exception("轉換公鑰失敗: " + ex.Message);
        }

        // 驗證簽章,假設使用 SHA256 與 PKCS#1 v1.5 padding
        bool isValid = rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        return isValid;
    }
}

使用範例

//準備資料
string message = @"產生PKCS1TEST 測試訊息";
byte[] data = Encoding.UTF8.GetBytes(message);

//進行簽章
byte[] signature = SignDataWithPKCS1(_projectId, _locationId, _keyRingId, _keyId, "1", data);
string signatureBase64 = Convert.ToBase64String(signature);
Console.WriteLine($"PKCS#1 :{signatureBase64}");

//驗證簽章
Console.WriteLine("測試驗證PKCS#1 ");
var result = VerifySignature(_projectId, _locationId, _keyRingId, _keyId, "1", data, signature);
Console.WriteLine($"驗證結果 :{result}");

C# PKCS#1轉 PKCS#7 無SignedAttributes

  1. 使用 Google HSM 產生 PKCS#1 簽章(參考上一節)
  2. 讀取cer檔 轉x509驗證
  3. PKCS#1 轉 PKCS#7
  4. 驗證

讀取cer檔 轉x509驗證

Google產生的cer檔案(憑證請求檔)其實是個Base64編碼過的PEM格式憑證,這個坑花了我超級多時間….。

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


/// <summary>
/// 萬用憑證載入器,支援 DER、PEM、Base64(無包裝)
/// </summary>
public static X509Certificate2 LoadCertificateFromPossiblyBase64Cer(string path)
{
    if (!File.Exists(path))
        throw new FileNotFoundException("找不到憑證檔案", path);

    byte[] raw = File.ReadAllBytes(path);

    // 1. 檢查是否為 PEM 格式
    string text = Encoding.ASCII.GetString(raw);
    if (text.Contains("-----BEGIN CERTIFICATE-----"))
    {
        return new X509Certificate2(raw); // PEM 支援
    }

    // 2. 嘗試將內容視為 Base64 編碼(無包裝)
    try
    {
        string b64 = Encoding.ASCII.GetString(raw)
            .Replace("\r", "").Replace("\n", "").Trim();

        byte[] der = Convert.FromBase64String(b64);
        return new X509Certificate2(der); // DER 匯入
    }
    catch
    {
        // 3. fallback: 當作原始二進位 DER 嘗試載入
        return new X509Certificate2(raw);
    }
}

然後讀取進來的憑證需要進行轉換為Org.BouncyCastle.X509.X509Certificate

var x509v2 = LoadCertificateFromPossiblyBase64Cer(@"C:\.....\googlehsm.cer");
var bcCert = parser.ReadCertificate(x509v2.RawData);

檢驗cer (pem) 主體

如果你想驗證這個檔案的話,直接驗證憑證主體就好,應該會出現簽章相關資訊

Console.WriteLine("憑證主體: " + x509v2.Subject);

透過Google HSM驗證cer檔是否有效

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


/// <summary>
/// 驗證 HSM 簽出的 PKCS#1 簽章
/// </summary>
public static bool VerifyPkcs1Signature(byte[] data, byte[] signature, X509Certificate2 cert)
{
    using RSA? rsa = cert.GetRSAPublicKey();
    if (rsa == null)
    {
        Console.WriteLine("無法從憑證取得 RSA 公鑰");
        return false;
    }

    bool result = rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
    Console.WriteLine(result ? "驗章成功!" : "驗章失敗!");
    return result;
}

在主程式進行驗證看看是否通過

string message = @"產生PKCS1TEST 測試訊息";
byte[] data = Encoding.UTF8.GetBytes(message);
byte[] signature = SignDataWithPKCS1(_projectId, _locationId, _keyRingId, _keyId, "1", data);

var x509v2 = LoadCertificateFromPossiblyBase64Cer(@"C:\..........\GoogleHSM.cer");

var result = VerifyPkcs1Signature(data, signature, x509v2);
Console.WriteLine($"驗證結果 :{result}");

PKCS#1 轉 PKCS#7 不含Attributes

using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Cms;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;


/// <summary>
/// 使用 BouncyCastle 建立 PKCS#7 Detached 簽章 (不含 SignedAttributes)
/// </summary>
public static byte[] CreatePkcs7FromPkcs1(byte[] data, byte[] pkcs1Signature, X509Certificate signerCert)
{
    // 1. 定義演算法 OID
    var oidData = new DerObjectIdentifier("1.2.840.113549.1.7.1"); // data
    var oidSignedData = new DerObjectIdentifier("1.2.840.113549.1.7.2"); // signedData
    var oidSha256 = new DerObjectIdentifier("2.16.840.1.101.3.4.2.1"); // SHA-256
    var oidRsaSha256 = new DerObjectIdentifier("1.2.840.113549.1.1.11"); // sha256WithRSAEncryption

    var digestAlgId = new AlgorithmIdentifier(oidSha256, DerNull.Instance);
    var sigAlgId = new AlgorithmIdentifier(oidRsaSha256, DerNull.Instance);

    // 2. 建立 SignerIdentifier(Issuer + SerialNumber)
    var issuerAndSerial = new IssuerAndSerialNumber(signerCert.IssuerDN, signerCert.SerialNumber);
    var signerIdentifier = new SignerIdentifier(issuerAndSerial);

    // 3. 組成 SignerInfo
    var signerInfoVector = new Asn1EncodableVector
    {
        new DerInteger(1), // version
        signerIdentifier.ToAsn1Object(),
        digestAlgId,
        sigAlgId,
        new DerOctetString(pkcs1Signature)
    };
    var signerInfo = new DerSequence(signerInfoVector);

    // 4. digestAlgorithms SET
    var digestAlgs = new DerSet(digestAlgId);

    // 5. contentInfo (detached 模式 content=null)
    var contentInfo = new ContentInfo(oidData, null);

    // 6. 包裝簽章憑證
    var certVector = new Asn1EncodableVector { signerCert.CertificateStructure };
    var certSet = new DerSet(certVector);
    var certTagged = new DerTaggedObject(false, 0, certSet); // [0] IMPLICIT CertificateSet

    // 7. SignerInfos SET
    var signerInfos = new DerSet(signerInfo);

    // 8. 組成 SignedData
    var signedDataVector = new Asn1EncodableVector
    {
        new DerInteger(3), // version
        digestAlgs,
        contentInfo,
        certTagged,
        signerInfos
    };
    var signedData = new DerSequence(signedDataVector);

    // 9. 組成 ContentInfo (封裝 signedData)
    var finalContentInfo = new ContentInfo(oidSignedData, signedData);

    // 10. DER 編碼後回傳
    return finalContentInfo.GetEncoded();
}

驗證PKCS#7 不含Attributes

/// <summary>
/// 驗證 PKCS#7 簽章 不含Attributes
/// </summary>
/// <param name="pkcs7Data"></param>
/// <param name="originalData"></param>
/// <returns></returns>
public static bool VerifyPkcs7(byte[] pkcs7Data, byte[] originalData)
{
    try
    {
        var contentInfo = new System.Security.Cryptography.Pkcs.ContentInfo(originalData);
        var signedCms = new System.Security.Cryptography.Pkcs.SignedCms(contentInfo, detached: true);
        signedCms.Decode(pkcs7Data);
        signedCms.CheckSignature(true);
        Console.WriteLine("PKCS#7 簽章驗證成功!");
        return true;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"PKCS#7 驗證失敗: {ex.Message}");
        return false;
    }
}

這邊要使用System.Security.Cryptography.Pkcs 這個套件驗證,原因是提供的除錯訊息比較多。

主程式

Console.WriteLine($"-----------------pkcs7-----------");
//準備資料
string message = @"產生PKCS7 測試訊息";
byte[] data = Encoding.UTF8.GetBytes(message);

//簽章PKCS#1
byte[] signature = SignDataWithPKCS1(_projectId, _locationId, _keyRingId, _keyId, "1", data);

//讀取憑證並轉換
var x509v2 = LoadCertificateFromPossiblyBase64Cer(@"C:\..........\GoogleHSM.cer");
var parser = new Org.BouncyCastle.X509.X509CertificateParser();
var bcCert = parser.ReadCertificate(x509v2.RawData);

//簽章PKCS#7
byte[] pkcs7Bytes = CreatePkcs7FromPkcs1(data, signature, bcCert);
string pkcs7Base64 = Convert.ToBase64String(pkcs7Bytes);

//驗證
VerifyPkcs7(pkcs7Bytes, data);

C# PKCS#1轉 PKCS#7 包含SignedAttributes

這邊複雜度很高,我花了非常多時間在此。

基本上PKCS#1那邊要改寫過才能用。

PKCS#7 用的 PKCS#1

/// <summary>
/// 使用 Google Cloud KMS 簽署 SHA256 雜湊
/// </summary>
/// <param name="projectId"></param>
/// <param name="locationId"></param>
/// <param name="keyRingId"></param>
/// <param name="keyId"></param>
/// <param name="keyVersion"></param>
/// <param name="signedAttributesDer"></param>
/// <param name="hashAlg"></param>
/// <returns></returns>
public static byte[] SignHashWithPKCS1(
    string projectId,
    string locationId,
    string keyRingId,
    string keyId,
    string keyVersion,
    byte[] signedAttributesDer, // <- DER encoded SignedAttributes
    HashAlgorithmName hashAlg)
{
    // 1. 先計算 SHA256 雜湊(這是你傳給 HSM 的資料)
    byte[] hash = SHA256.HashData(signedAttributesDer);

    // 2. 建立 KMS 用戶端
    var client = KeyManagementServiceClient.Create();

    // 3. 指定要用的 HSM 金鑰版本
    var keyVersionName = new CryptoKeyVersionName(projectId, locationId, keyRingId, keyId, keyVersion);

    // 4. 傳入雜湊值給 Google HSM
    var digest = new Digest
    {
        Sha256 = ByteString.CopyFrom(hash)
    };

    // 5. 呼叫 AsymmetricSign API
    AsymmetricSignResponse response = client.AsymmetricSign(keyVersionName, digest);

    return response.Signature.ToByteArray();
}

PKCS#1轉 PKCS#7 加上 Arributes

/// <summary>
/// 自動產生 SignedAttributes、透過 HSM 簽章並組成帶 Attributes 的 PKCS#7 簽章
/// </summary>
/// <param name="data">原始資料</param>
/// <param name="signerCert">簽章者憑證(BouncyCastle X509)</param>
/// <param name="signHashFunc">Func 傳入 hash,回傳 HSM 簽章 byte[]</param>
/// <returns>PKCS#7 (CMS SignedData with SignedAttributes)</returns>
public static byte[] GeneratePkcs7WithSignedAttributesFromHsm(
byte[] data,
X509Certificate signerCert,
Func<byte[], byte[]> signHashFunc)
{
    var oidData = new DerObjectIdentifier("1.2.840.113549.1.7.1");       // data
    var oidSignedData = new DerObjectIdentifier("1.2.840.113549.1.7.2"); // signedData
    var oidSha256 = new DerObjectIdentifier("2.16.840.1.101.3.4.2.1");   // sha256
    var oidRsaSha256 = new DerObjectIdentifier("1.2.840.113549.1.1.11"); // sha256WithRSAEncryption

    var digestAlgId = new AlgorithmIdentifier(oidSha256, DerNull.Instance);
    var sigAlgId = new AlgorithmIdentifier(oidRsaSha256, DerNull.Instance);

    // === 1. 組 SignedAttributes ===
    var attrVector = new Asn1EncodableVector();

    // contentType
    attrVector.Add(new Org.BouncyCastle.Asn1.Cms.Attribute(
        new DerObjectIdentifier("1.2.840.113549.1.9.3"),
        new DerSet(oidData)));

    // messageDigest
    byte[] hashOfData = DigestUtilities.CalculateDigest("SHA-256", data);
    attrVector.Add(new Org.BouncyCastle.Asn1.Cms.Attribute(
        new DerObjectIdentifier("1.2.840.113549.1.9.4"),
        new DerSet(new DerOctetString(hashOfData))));

    var signedAttributes = new DerSet(attrVector);
    byte[] signedAttrBytes = signedAttributes.GetEncoded();

    // === 2. HSM 簽章:直接傳入 DER 編碼的 SignedAttributes
    byte[] pkcs1Signature = signHashFunc(signedAttrBytes);

    // === 3. SignerInfo ===
    var issuerAndSerial = new IssuerAndSerialNumber(signerCert.IssuerDN, signerCert.SerialNumber);
    var signerIdentifier = new SignerIdentifier(issuerAndSerial);

    var signerInfo = new DerSequence(new Asn1Encodable[]
    {
    new DerInteger(3),
    signerIdentifier.ToAsn1Object(),
    digestAlgId,
    new DerTaggedObject(false, 0, signedAttributes),
    sigAlgId,
    new DerOctetString(pkcs1Signature)
    });

    // === 4. SignedData ===
    var digestAlgs = new DerSet(digestAlgId);
    var contentInfo = new ContentInfo(oidData, null);

    var certVector = new Asn1EncodableVector { signerCert.CertificateStructure };
    var certSet = new DerSet(certVector);
    var certTagged = new DerTaggedObject(false, 0, certSet);

    var signerInfos = new DerSet(signerInfo);

    var signedData = new DerSequence(new Asn1Encodable[]
    {
    new DerInteger(3),
    digestAlgs,
    contentInfo,
    certTagged,
    signerInfos
    });

    var finalContentInfo = new ContentInfo(oidSignedData, signedData);
    return finalContentInfo.GetEncoded();
}

驗證PKCS#7簽章

/// <summary>
/// 驗證 PKCS#7(CMS SignedData)簽章,包含 SignedAttributes 驗證
/// </summary>
/// <param name="pkcs7">PKCS#7 簽章資料(含簽章與憑證)</param>
/// <param name="originalData">原始資料(被簽的內容)</param>
public static bool VerifyPkcs7WithAttributes(byte[] pkcs7, byte[] originalData)
{
    try
    {
        var contentInfo = new ContentInfo(originalData);
        var signedCms = new SignedCms(contentInfo, detached: true);
        signedCms.Decode(pkcs7);

        // true = 嚴格驗證 signed attributes(如 messageDigest, contentType)
        signedCms.CheckSignature(true);

        Console.WriteLine("簽章驗證成功(含 SignedAttributes)");
        return true;
    }
    catch (CryptographicException ex)
    {
        Console.WriteLine("簽章驗證失敗(含 SignedAttributes):" + ex.Message);
        return false;
    }
}

組合使用

Console.WriteLine($"-----------------pkcs7-----------");
//準備資料
string message = @"產生PKCS7 測試訊息";
byte[] data = Encoding.UTF8.GetBytes(message);

//簽章PKCS#1
byte[] signature = SignDataWithPKCS1(_projectId, _locationId, _keyRingId, _keyId, "1", data);

//讀取憑證並轉換
var x509v2 = LoadCertificateFromPossiblyBase64Cer(@"C:\..........\GoogleHSM.cer");
var parser = new Org.BouncyCastle.X509.X509CertificateParser();
var bcCert = parser.ReadCertificate(x509v2.RawData);

//簽章PKCS#7
static byte[] signHashFunc(byte[] hash) =>
                SignHashWithPKCS1(_projectId, _locationId, _keyRingId, _keyId, "1", hash, HashAlgorithmName.SHA256);

byte[] pkcs7WithAttrs = GeneratePkcs7WithSignedAttributesFromHsm(data, bcCert, signHashFunc);

//驗證
VerifyPkcs7WithAttributes(pkcs7WithAttrs, data);

參考資料

  1. RFC3369 這是PKCS標準有關的文件
  2. 微軟MSDN文件
  3. 自然人憑證開發筆記 這邊也有名詞解釋
  4. 認識 PKI 架構下的數位憑證格式與憑證格式轉換的心得分享 這邊對於名詞解釋可以參考
  5. 維基百科-PKCS
  6. 維基百科-Padding (cryptography)
  7. Bouncy Castle C# 文件
  8. OpenAI – ChatGPT

0 Comments

Submit a Comment

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *