win-acme手动执行finalize签发证书
一、前言
在使用win-acme续订证书时,在finalize步骤前发生了错误导致续订失败,再次续订时因ACME Renewal Information(ARI)机制导致无法续订;遂尝试手动执行finalize步骤签发证书。
{
"type": "urn:ietf:params:acme:error:alreadyReplaced",
"detail": "Could not validate ARI 'replaces' field :: cannot indicate an order replaces certificate with serial \"abcd\", which already has a replacement order",
"status": 409
}
win-acme的数据存储在%ProgramData%\win-acme\<ACME-Server-Domain>文件夹中(例如C:\ProgramData\win-acme\acme-v02.api.letsencrypt.org),后续操作均在该文件夹中进行。
二、获取订单
(一)获取订单链接
在Logs文件夹中的最新日志文件中搜索https://acme-v02.api.letsencrypt.org/acme/order,找到后在浏览器中打开完整链接(例如https://acme-v02.api.letsencrypt.org/acme/order/12345678/12345678),检查status键的值是否为ready。如果为valid,则已完成证书签发。
finalize链接为finalize键的值(例如https://acme-v02.api.letsencrypt.org/acme/finalize/12345678/12345678)
(二)获取CSR
CSR文件储存在Certificates文件夹中,其文件名格式为<续订ID>-<域名>-<UUID>-csr.pem。
三、获取帐户
(一)帐户ID
帐户ID存储在JSON格式的Registration_v2文件中,为Kid键的值(例如https://acme-v02.api.letsencrypt.org/acme/acct/12345678)。
(二)帐户私钥
帐户私钥经Data Protection API(DPAPI)加密后经base64编码存储在Signer_v2文件中,其内容格式为enc-<Base64字符串>。可以在PowerShell中运行以下命令进行解密:
$filePath = "Signer_v2"
$encryptedBase64 = Get-Content -Path $filePath
$encryptedBase64 = $encryptedBase64.Trim() -replace '^enc-', ''
$encryptedBytes = [Convert]::FromBase64String($encryptedBase64)
Add-Type -AssemblyName System.Security
$decryptedBytes = [System.Security.Cryptography.ProtectedData]::Unprotect(
$encryptedBytes, $null,
[System.Security.Cryptography.DataProtectionScope]::LocalMachine
)
$decrypted = [System.Text.Encoding]::UTF8.GetString($decryptedBytes)
Set-Content -Path ($filePath + ".txt") -Value $decrypted
解密后帐户私钥以JSON格式存储在Signer_v2.txt文件中,其内容格式如下:
{"KeyType": "ES256", "KeyExport": "{\u0022HashSize\u0022: 256, \u0022D\u0022: \u0022abcd\u0022, \u0022X\u0022: \u0022abcd\u0022, \u0022Y\u0022: \u0022abcd\u0022}"}
可以通过以下Python脚本获取ECDSA私钥:
import base64
import json
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
key_file_path = "Signer_v2.txt"
with open(key_file_path, 'r') as file:
key_data = json.load(file)
assert(key_data['KeyType'] == 'ES256')
key_export = json.loads(key_data['KeyExport'])
d = int.from_bytes(base64.b64decode(key_export['D']))
x = int.from_bytes(base64.b64decode(key_export['X']))
y = int.from_bytes(base64.b64decode(key_export['Y']))
public_numbers = ec.EllipticCurvePublicNumbers(x=x, y=y, curve=ec.SECP256R1())
private_numbers = ec.EllipticCurvePrivateNumbers(private_value=d, public_numbers=public_numbers)
private_key = private_numbers.private_key()
private_key_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
with open(key_file_path + '.pem', 'wb') as file:
file.write(private_key_pem)
四、执行finalize
可以通过以下Python脚本执行finalize:
import base64
import json
import requests
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, utils
FINALIZE_URL = "<finalize链接>"
CSR_FILE_PATH = "<CSR文件路径>"
KID = "<帐户ID>"
ACCOUNT_KEY_FILE_PATH = "Signer_v2.txt.pem"
with open(CSR_FILE_PATH, 'rb') as file:
csr_data = x509.load_pem_x509_csr(file.read())
csr_b64 = base64.urlsafe_b64encode(csr_data.public_bytes(serialization.Encoding.DER)).decode()
with open(ACCOUNT_KEY_FILE_PATH, 'rb') as file:
account_key = serialization.load_pem_private_key(file.read(), None)
nonce_response = requests.head("https://acme-v02.api.letsencrypt.org/acme/new-nonce")
nonce = nonce_response.headers['Replay-Nonce']
protected = {
"alg": "ES256",
"kid": KID,
"nonce": nonce,
"url": FINALIZE_URL
}
protected_b64 = base64.urlsafe_b64encode(json.dumps(protected).encode()).decode().rstrip('=')
payload = {"csr": csr_b64}
payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=')
signing_input = f"{protected_b64}.{payload_b64}".encode()
signature_der = account_key.sign(signing_input, ec.ECDSA(hashes.SHA256()))
signature_r, signature_s = utils.decode_dss_signature(signature_der)
signature_raw = signature_r.to_bytes(32, 'big') + signature_s.to_bytes(32, 'big')
signature_b64 = base64.urlsafe_b64encode(signature_raw).decode().rstrip('=')
jws = {
"protected": protected_b64,
"payload": payload_b64,
"signature": signature_b64
}
response = requests.post(FINALIZE_URL, json=jws, headers={"Content-Type": "application/jose+json"})
print(response.text)
如果执行成功,刷新order链接页面,如果status键的值为ready则成功执行了finalize,可由certificate键的值下载证书。
五、后话
下载证书后,发现私钥已经被win-acme删了。最后删除了Certificates文件夹、把<续订ID>.renewal.json的certificate键的数组清空就能续订了。南辕北辙,白搞!

浙公网安备 33010602011771号