Views: 16
這個我弄蠻很久,覺得坑很多,這邊前前後後大概花了有三週才弄好簽章轉換,把踩過的坑紀錄在這邊。
聲明
目前我還在處於跟銀行對接測試,這篇給需要的人參考就好
完整程式碼
請參考我Github這個專案。
環境
- Windows 11 專業版 23H2
- .Net 9
- Visual Studio 2022
前置作業
- Google HSM那邊產出對應的csr檔跟json檔,以及一些設定資訊
- 開發環境跟運作環境要把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 為例):
- 對原始資料做
SHA-256
雜湊 - 使用 RSA 私鑰對雜湊結果簽章
- 結果是一個固定長度(如 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#對這塊還不是很完整,需要透過第三方套件
Google.Cloud.Kms.V1
3.6Portable.BouncyCastle
1.9System.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 簽章
- 使用 Google HSM 產生 PKCS#1 簽章
- 驗證 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
- 使用 Google HSM 產生 PKCS#1 簽章(參考上一節)
- 讀取cer檔 轉x509驗證
- PKCS#1 轉 PKCS#7
- 驗證
讀取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);
參考資料
- RFC3369 這是PKCS標準有關的文件
- 微軟MSDN文件
- 自然人憑證開發筆記 這邊也有名詞解釋
- 認識 PKI 架構下的數位憑證格式與憑證格式轉換的心得分享 這邊對於名詞解釋可以參考
- 維基百科-PKCS
- 維基百科-Padding (cryptography)
- Bouncy Castle C# 文件
- OpenAI – ChatGPT
0 Comments