diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 000000000..7e709f912 --- /dev/null +++ b/sdk/README.md @@ -0,0 +1,89 @@ +# Certd OpenAPI SDK 示例 + +本目录提供 `/api/v1/cert/get` 接口的多语言 SDK 示例,演示如何封装 `CertdClient`,生成 `x-certd-token`,并按域名或证书 ID 获取证书。 + +## 目录结构 + +| 语言 | SDK 类/类型 | 调用示例 | +| --- | --- | --- | +| Node.js | `sdk/nodejs/certd-client.js` | `sdk/nodejs/get-cert.js` | +| Python | `sdk/python/certd_client.py` | `sdk/python/get_cert.py` | +| Go | `sdk/go/certd_client.go` | `sdk/go/get_cert.go` | +| PHP | `sdk/php/CertdClient.php` | `sdk/php/get_cert.php` | +| Java | `sdk/java/CertdClient.java` | `sdk/java/GetCert.java` | + +`CertdClient` 提供以下核心方法: + +- `getSign(content)` / `GetSign(content)`:根据 `content + keySecret` 生成签名 +- `getToken()` / `GetToken()`:生成 `x-certd-token` +- `request(path, body)` / `Request(path, body)`:携带 token 发起 OpenAPI 请求 +- `getCert(params)` / `GetCert(params)`:调用 `/api/v1/cert/get` 的便捷方法 + +## 接口说明 + +- 请求地址:`POST /api/v1/cert/get` +- 认证方式:请求头传入 `x-certd-token` +- `certId` 和 `domains` 至少传一个;两个都传时,服务端优先使用 `certId` +- `autoApply=true` 时,如果没有可用证书,会尝试自动创建或触发流水线申请证书 +- `format` 可选:`pem`、`jks`、`pfx`、`der`、`one`、`p7b` + +## Token 生成规则 + +1. 在 OpenKey 页面生成 `keyId` 和 `keySecret` +2. 构造 JSON 字符串: + +```json +{"keyId":"你的 keyId","t":1710000000,"encrypt":false,"signType":"md5"} +``` + +3. 签名:`sign = md5(content + keySecret)` +4. Header:`x-certd-token = base64(content) + "." + base64(sign)` + +注意:签名时必须使用和 base64 编码时完全相同的 `content` 字符串。 + +## 环境变量 + +所有示例都支持以下环境变量: + +| 变量 | 说明 | 默认值 | +| --- | --- | --- | +| `CERTD_BASE_URL` | Certd 服务地址 | `http://127.0.0.1:7001` | +| `CERTD_KEY_ID` | OpenKey 的 keyId | 必填 | +| `CERTD_KEY_SECRET` | OpenKey 的 keySecret | 必填 | +| `CERTD_DOMAINS` | 域名列表,多个用英文逗号隔开 | 空 | +| `CERTD_CERT_ID` | 证书仓库证书 ID | 空 | +| `CERTD_AUTO_APPLY` | 不存在或过期时是否自动申请 | `false` | +| `CERTD_FORMAT` | 返回证书格式 | 空,表示返回所有格式 | +| `CERTD_ENCRYPT` | 是否要求接口加密返回结果 | `false` | + +## 运行示例 + +PowerShell: + +```powershell +$env:CERTD_BASE_URL = "http://127.0.0.1:7001" +$env:CERTD_KEY_ID = "your_key_id" +$env:CERTD_KEY_SECRET = "your_key_secret" +$env:CERTD_DOMAINS = "example.com,*.example.com" +$env:CERTD_AUTO_APPLY = "true" +$env:CERTD_FORMAT = "pem" + +node sdk\nodejs\get-cert.js +python sdk\python\get_cert.py +go run sdk\go\certd_client.go sdk\go\get_cert.go +php sdk\php\get_cert.php +javac sdk\java\CertdClient.java sdk\java\GetCert.java && java -cp sdk\java GetCert +``` + +如果使用 `CERTD_CERT_ID` 获取证书,可以不传 `CERTD_DOMAINS`: + +```powershell +$env:CERTD_CERT_ID = "1" +$env:CERTD_DOMAINS = "" +``` + +## 返回结果 + +`CERTD_ENCRYPT=false` 时,示例会直接打印接口返回的 JSON。 + +`CERTD_ENCRYPT=true` 时,接口返回内容会被服务端加密;这些示例只演示请求和 token 生成,不包含解密逻辑。 diff --git a/sdk/go/certd_client.go b/sdk/go/certd_client.go new file mode 100644 index 000000000..62407e044 --- /dev/null +++ b/sdk/go/certd_client.go @@ -0,0 +1,107 @@ +package main + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +type CertdClient struct { + KeyID string + KeySecret string + BaseURL string + Encrypt bool + SignType string +} + +type tokenContent struct { + KeyID string `json:"keyId"` + T int64 `json:"t"` + Encrypt bool `json:"encrypt"` + SignType string `json:"signType"` +} + +func NewCertdClient(keyID, keySecret string) (*CertdClient, error) { + if keyID == "" { + return nil, fmt.Errorf("keyID is required") + } + if keySecret == "" { + return nil, fmt.Errorf("keySecret is required") + } + return &CertdClient{ + KeyID: keyID, + KeySecret: keySecret, + BaseURL: "http://127.0.0.1:7001", + SignType: "md5", + }, nil +} + +func (c *CertdClient) GetSign(content string) (string, error) { + if c.SignType != "md5" { + return "", fmt.Errorf("unsupported signType: %s", c.SignType) + } + sum := md5.Sum([]byte(content + c.KeySecret)) + return hex.EncodeToString(sum[:]), nil +} + +func (c *CertdClient) GetToken() (string, error) { + contentBytes, err := json.Marshal(tokenContent{ + KeyID: c.KeyID, + T: time.Now().Unix(), + Encrypt: c.Encrypt, + SignType: c.SignType, + }) + if err != nil { + return "", err + } + content := string(contentBytes) + sign, err := c.GetSign(content) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString([]byte(content)) + "." + base64.StdEncoding.EncodeToString([]byte(sign)), nil +} + +func (c *CertdClient) Request(path string, body any) ([]byte, error) { + bodyBytes, err := json.Marshal(body) + if err != nil { + return nil, err + } + token, err := c.GetToken() + if err != nil { + return nil, err + } + + httpReq, err := http.NewRequest(http.MethodPost, strings.TrimRight(c.BaseURL, "/")+path, bytes.NewReader(bodyBytes)) + if err != nil { + return nil, err + } + httpReq.Header.Set("content-type", "application/json") + httpReq.Header.Set("x-certd-token", token) + + resp, err := http.DefaultClient.Do(httpReq) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)) + } + return respBody, nil +} + +func (c *CertdClient) GetCert(params any) ([]byte, error) { + return c.Request("/api/v1/cert/get", params) +} diff --git a/sdk/go/get_cert.go b/sdk/go/get_cert.go new file mode 100644 index 000000000..76d9a2f66 --- /dev/null +++ b/sdk/go/get_cert.go @@ -0,0 +1,85 @@ +package main + +import ( + "fmt" + "os" + "strconv" + "strings" +) + +type certGetRequest struct { + Domains string `json:"domains,omitempty"` + CertID int64 `json:"certId,omitempty"` + AutoApply bool `json:"autoApply"` + Format string `json:"format,omitempty"` +} + +func requireEnv(name string) (string, error) { + value := os.Getenv(name) + if value == "" { + return "", fmt.Errorf("missing environment variable: %s", name) + } + return value, nil +} + +func boolEnv(name string, defaultValue bool) bool { + value := os.Getenv(name) + if value == "" { + return defaultValue + } + switch strings.ToLower(value) { + case "1", "true", "yes", "y": + return true + default: + return false + } +} + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run() error { + keyID, err := requireEnv("CERTD_KEY_ID") + if err != nil { + return err + } + keySecret, err := requireEnv("CERTD_KEY_SECRET") + if err != nil { + return err + } + + client, err := NewCertdClient(keyID, keySecret) + if err != nil { + return err + } + if baseURL := os.Getenv("CERTD_BASE_URL"); baseURL != "" { + client.BaseURL = baseURL + } + client.Encrypt = boolEnv("CERTD_ENCRYPT", false) + + reqBody := certGetRequest{ + Domains: os.Getenv("CERTD_DOMAINS"), + AutoApply: boolEnv("CERTD_AUTO_APPLY", false), + Format: os.Getenv("CERTD_FORMAT"), + } + if certID := os.Getenv("CERTD_CERT_ID"); certID != "" { + reqBody.CertID, err = strconv.ParseInt(certID, 10, 64) + if err != nil || reqBody.CertID <= 0 { + return fmt.Errorf("CERTD_CERT_ID must be a positive integer") + } + } + if reqBody.CertID == 0 && reqBody.Domains == "" { + return fmt.Errorf("set CERTD_CERT_ID or CERTD_DOMAINS") + } + + respBody, err := client.GetCert(reqBody) + if err != nil { + return err + } + fmt.Println(string(respBody)) + return nil +} diff --git a/sdk/java/CertdClient.java b/sdk/java/CertdClient.java new file mode 100644 index 000000000..ab1c5605d --- /dev/null +++ b/sdk/java/CertdClient.java @@ -0,0 +1,127 @@ +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Instant; +import java.util.Base64; + +public class CertdClient { + private final String keyId; + private final String keySecret; + private final String baseUrl; + private final boolean encrypt; + private final String signType; + + public CertdClient(String keyId, String keySecret) { + this(keyId, keySecret, "http://127.0.0.1:7001", false); + } + + public CertdClient(String keyId, String keySecret, String baseUrl, boolean encrypt) { + if (isBlank(keyId)) { + throw new IllegalArgumentException("keyId is required"); + } + if (isBlank(keySecret)) { + throw new IllegalArgumentException("keySecret is required"); + } + this.keyId = keyId; + this.keySecret = keySecret; + this.baseUrl = trimRightSlash(isBlank(baseUrl) ? "http://127.0.0.1:7001" : baseUrl); + this.encrypt = encrypt; + this.signType = "md5"; + } + + public String getSign(String content) throws Exception { + if (!"md5".equals(signType)) { + throw new IllegalArgumentException("Unsupported signType: " + signType); + } + return md5Hex(content + keySecret); + } + + public String getToken() throws Exception { + String content = "{\"keyId\":\"" + jsonEscape(keyId) + "\",\"t\":" + Instant.now().getEpochSecond() + + ",\"encrypt\":" + encrypt + ",\"signType\":\"" + signType + "\"}"; + String sign = getSign(content); + return base64(content) + "." + base64(sign); + } + + public String request(String path, String bodyJson) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .header("content-type", "application/json") + .header("x-certd-token", getToken()) + .POST(HttpRequest.BodyPublishers.ofString(bodyJson, StandardCharsets.UTF_8)) + .build(); + + HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new IOException("HTTP " + response.statusCode() + ": " + response.body()); + } + return response.body(); + } + + public String getCert(String paramsJson) throws Exception { + return request("/api/v1/cert/get", paramsJson); + } + + public static String jsonEscape(String value) { + StringBuilder escaped = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + switch (ch) { + case '"': + escaped.append("\\\""); + break; + case '\\': + escaped.append("\\\\"); + break; + case '\b': + escaped.append("\\b"); + break; + case '\f': + escaped.append("\\f"); + break; + case '\n': + escaped.append("\\n"); + break; + case '\r': + escaped.append("\\r"); + break; + case '\t': + escaped.append("\\t"); + break; + default: + if (ch < 0x20) { + escaped.append(String.format("\\u%04x", (int) ch)); + } else { + escaped.append(ch); + } + } + } + return escaped.toString(); + } + + private static String md5Hex(String value) throws Exception { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(value.getBytes(StandardCharsets.UTF_8)); + StringBuilder hex = new StringBuilder(); + for (byte b : digest) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } + + private static String base64(String value) { + return Base64.getEncoder().encodeToString(value.getBytes(StandardCharsets.UTF_8)); + } + + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } + + private static String trimRightSlash(String value) { + return value.replaceAll("/+$", ""); + } +} diff --git a/sdk/java/GetCert.java b/sdk/java/GetCert.java new file mode 100644 index 000000000..41e13fd7e --- /dev/null +++ b/sdk/java/GetCert.java @@ -0,0 +1,84 @@ +public class GetCert { + public static void main(String[] args) { + try { + new GetCert().run(); + } catch (Exception e) { + System.err.println(e.getMessage()); + System.exit(1); + } + } + + private void run() throws Exception { + CertdClient client = new CertdClient( + requireEnv("CERTD_KEY_ID"), + requireEnv("CERTD_KEY_SECRET"), + env("CERTD_BASE_URL", "http://127.0.0.1:7001"), + boolEnv("CERTD_ENCRYPT", false) + ); + + String certId = System.getenv("CERTD_CERT_ID"); + String domains = System.getenv("CERTD_DOMAINS"); + String format = System.getenv("CERTD_FORMAT"); + + if (isBlank(certId) && isBlank(domains)) { + throw new IllegalArgumentException("Set CERTD_CERT_ID or CERTD_DOMAINS"); + } + + StringBuilder body = new StringBuilder(); + body.append("{"); + boolean hasField = false; + if (!isBlank(certId)) { + body.append("\"certId\":").append(Long.parseLong(certId)); + hasField = true; + } + if (!isBlank(domains)) { + appendComma(body, hasField); + body.append("\"domains\":\"").append(CertdClient.jsonEscape(domains)).append("\""); + hasField = true; + } + appendComma(body, hasField); + body.append("\"autoApply\":").append(boolEnv("CERTD_AUTO_APPLY", false)); + hasField = true; + if (!isBlank(format)) { + appendComma(body, hasField); + body.append("\"format\":\"").append(CertdClient.jsonEscape(format)).append("\""); + } + body.append("}"); + + System.out.println(client.getCert(body.toString())); + } + + private static String requireEnv(String name) { + String value = System.getenv(name); + if (isBlank(value)) { + throw new IllegalArgumentException("Missing environment variable: " + name); + } + return value; + } + + private static String env(String name, String defaultValue) { + String value = System.getenv(name); + return isBlank(value) ? defaultValue : value; + } + + private static boolean boolEnv(String name, boolean defaultValue) { + String value = System.getenv(name); + if (isBlank(value)) { + return defaultValue; + } + return value.equalsIgnoreCase("1") + || value.equalsIgnoreCase("true") + || value.equalsIgnoreCase("yes") + || value.equalsIgnoreCase("y"); + } + + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } + + private static void appendComma(StringBuilder builder, boolean hasField) { + if (hasField) { + builder.append(","); + } + } +} diff --git a/sdk/nodejs/certd-client.js b/sdk/nodejs/certd-client.js new file mode 100644 index 000000000..5b698202f --- /dev/null +++ b/sdk/nodejs/certd-client.js @@ -0,0 +1,60 @@ +const crypto = require("crypto"); + +class CertdClient { + constructor(keyId, keySecret, options = {}) { + if (!keyId) { + throw new Error("keyId is required"); + } + if (!keySecret) { + throw new Error("keySecret is required"); + } + this.keyId = keyId; + this.keySecret = keySecret; + this.baseUrl = (options.baseUrl || "http://127.0.0.1:7001").replace(/\/$/, ""); + this.encrypt = options.encrypt === true; + this.signType = options.signType || "md5"; + } + + getSign(content) { + if (this.signType !== "md5") { + throw new Error(`Unsupported signType: ${this.signType}`); + } + return crypto.createHash("md5").update(content + this.keySecret, "utf8").digest("hex"); + } + + getToken(options = {}) { + const encrypt = options.encrypt ?? this.encrypt; + const content = JSON.stringify({ + keyId: this.keyId, + t: Math.floor(Date.now() / 1000), + encrypt, + signType: this.signType, + }); + const sign = this.getSign(content); + return `${Buffer.from(content, "utf8").toString("base64")}.${Buffer.from(sign, "utf8").toString("base64")}`; + } + + async request(path, body = {}, options = {}) { + const response = await fetch(`${this.baseUrl}${path}`, { + method: options.method || "POST", + headers: { + "content-type": "application/json", + "x-certd-token": this.getToken({ encrypt: options.encrypt }), + ...(options.headers || {}), + }, + body: JSON.stringify(body), + }); + + const text = await response.text(); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${text}`); + } + return text; + } + + getCert(params) { + return this.request("/api/v1/cert/get", params); + } +} + +module.exports = { CertdClient }; diff --git a/sdk/nodejs/get-cert.js b/sdk/nodejs/get-cert.js new file mode 100644 index 000000000..ef0ec6a10 --- /dev/null +++ b/sdk/nodejs/get-cert.js @@ -0,0 +1,52 @@ +#!/usr/bin/env node + +const { CertdClient } = require("./certd-client"); + +function requireEnv(name) { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing environment variable: ${name}`); + } + return value; +} + +function boolEnv(name, defaultValue = false) { + const value = process.env[name]; + if (value == null || value === "") { + return defaultValue; + } + return ["1", "true", "yes", "y"].includes(value.toLowerCase()); +} + +async function main() { + const client = new CertdClient(requireEnv("CERTD_KEY_ID"), requireEnv("CERTD_KEY_SECRET"), { + baseUrl: process.env.CERTD_BASE_URL, + encrypt: boolEnv("CERTD_ENCRYPT"), + }); + + const params = { + autoApply: boolEnv("CERTD_AUTO_APPLY"), + }; + if (process.env.CERTD_CERT_ID) { + params.certId = Number(process.env.CERTD_CERT_ID); + if (!Number.isInteger(params.certId) || params.certId <= 0) { + throw new Error("CERTD_CERT_ID must be a positive integer"); + } + } + if (process.env.CERTD_DOMAINS) { + params.domains = process.env.CERTD_DOMAINS; + } + if (process.env.CERTD_FORMAT) { + params.format = process.env.CERTD_FORMAT; + } + if (!params.certId && !params.domains) { + throw new Error("Set CERTD_CERT_ID or CERTD_DOMAINS"); + } + + console.log(await client.getCert(params)); +} + +main().catch(error => { + console.error(error.message); + process.exit(1); +}); diff --git a/sdk/php/CertdClient.php b/sdk/php/CertdClient.php new file mode 100644 index 000000000..507c80465 --- /dev/null +++ b/sdk/php/CertdClient.php @@ -0,0 +1,84 @@ +keyId = $keyId; + $this->keySecret = $keySecret; + $this->baseUrl = rtrim($options['baseUrl'] ?? 'http://127.0.0.1:7001', '/'); + $this->encrypt = $options['encrypt'] ?? false; + $this->signType = $options['signType'] ?? 'md5'; + } + + public function getSign(string $content): string + { + if ($this->signType !== 'md5') { + throw new InvalidArgumentException("Unsupported signType: {$this->signType}"); + } + return md5($content . $this->keySecret); + } + + public function getToken(?bool $encrypt = null): string + { + $content = json_encode([ + 'keyId' => $this->keyId, + 't' => time(), + 'encrypt' => $encrypt ?? $this->encrypt, + 'signType' => $this->signType, + ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $sign = $this->getSign($content); + return base64_encode($content) . '.' . base64_encode($sign); + } + + public function request(string $path, array $body = [], ?bool $encrypt = null): string + { + if (!function_exists('curl_init')) { + throw new RuntimeException('PHP curl extension is required'); + } + + $payload = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $headers = [ + 'Content-Type: application/json', + 'x-certd-token: ' . $this->getToken($encrypt), + ]; + + $ch = curl_init($this->baseUrl . $path); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 60, + ]); + + $response = curl_exec($ch); + if ($response === false) { + throw new RuntimeException(curl_error($ch)); + } + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($statusCode < 200 || $statusCode >= 300) { + throw new RuntimeException("HTTP {$statusCode}: {$response}"); + } + return $response; + } + + public function getCert(array $params): string + { + return $this->request('/api/v1/cert/get', $params); + } +} diff --git a/sdk/php/get_cert.php b/sdk/php/get_cert.php new file mode 100644 index 000000000..544e4bae9 --- /dev/null +++ b/sdk/php/get_cert.php @@ -0,0 +1,52 @@ + getenv('CERTD_BASE_URL') ?: 'http://127.0.0.1:7001', + 'encrypt' => bool_env('CERTD_ENCRYPT'), + ]); + + $params = [ + 'autoApply' => bool_env('CERTD_AUTO_APPLY'), + ]; + if (getenv('CERTD_CERT_ID')) { + if (!ctype_digit(getenv('CERTD_CERT_ID'))) { + throw new RuntimeException('CERTD_CERT_ID must be a positive integer'); + } + $params['certId'] = intval(getenv('CERTD_CERT_ID')); + } + if (getenv('CERTD_DOMAINS')) { + $params['domains'] = getenv('CERTD_DOMAINS'); + } + if (getenv('CERTD_FORMAT')) { + $params['format'] = getenv('CERTD_FORMAT'); + } + if (empty($params['certId']) && empty($params['domains'])) { + throw new RuntimeException('Set CERTD_CERT_ID or CERTD_DOMAINS'); + } + + echo $client->getCert($params) . PHP_EOL; +} catch (Throwable $e) { + fwrite(STDERR, $e->getMessage() . PHP_EOL); + exit(1); +} diff --git a/sdk/python/certd_client.py b/sdk/python/certd_client.py new file mode 100644 index 000000000..fa36f1474 --- /dev/null +++ b/sdk/python/certd_client.py @@ -0,0 +1,81 @@ +import base64 +import hashlib +import json +import time +import urllib.error +import urllib.request +from typing import Any, Dict, Optional + + +class CertdClient: + def __init__( + self, + key_id: str, + key_secret: str, + base_url: str = "http://127.0.0.1:7001", + encrypt: bool = False, + sign_type: str = "md5", + ) -> None: + if not key_id: + raise ValueError("key_id is required") + if not key_secret: + raise ValueError("key_secret is required") + self.key_id = key_id + self.key_secret = key_secret + self.base_url = base_url.rstrip("/") + self.encrypt = encrypt + self.sign_type = sign_type + + def get_sign(self, content: str) -> str: + if self.sign_type != "md5": + raise ValueError(f"Unsupported sign_type: {self.sign_type}") + return hashlib.md5((content + self.key_secret).encode("utf-8")).hexdigest() + + def getSign(self, content: str) -> str: + return self.get_sign(content) + + def get_token(self, encrypt: Optional[bool] = None) -> str: + content = json.dumps( + { + "keyId": self.key_id, + "t": int(time.time()), + "encrypt": self.encrypt if encrypt is None else encrypt, + "signType": self.sign_type, + }, + separators=(",", ":"), + ensure_ascii=False, + ) + sign = self.get_sign(content) + return ( + base64.b64encode(content.encode("utf-8")).decode("ascii") + + "." + + base64.b64encode(sign.encode("utf-8")).decode("ascii") + ) + + def getToken(self, encrypt: Optional[bool] = None) -> str: + return self.get_token(encrypt) + + def request(self, path: str, body: Optional[Dict[str, Any]] = None, encrypt: Optional[bool] = None) -> str: + data = json.dumps(body or {}, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + request = urllib.request.Request( + f"{self.base_url}{path}", + data=data, + method="POST", + headers={ + "content-type": "application/json", + "x-certd-token": self.get_token(encrypt), + }, + ) + + try: + with urllib.request.urlopen(request, timeout=60) as response: + return response.read().decode("utf-8") + except urllib.error.HTTPError as error: + message = error.read().decode("utf-8", errors="replace") + raise RuntimeError(f"HTTP {error.code}: {message}") from error + + def get_cert(self, params: Dict[str, Any]) -> str: + return self.request("/api/v1/cert/get", params) + + def getCert(self, params: Dict[str, Any]) -> str: + return self.get_cert(params) diff --git a/sdk/python/get_cert.py b/sdk/python/get_cert.py new file mode 100644 index 000000000..858a6debf --- /dev/null +++ b/sdk/python/get_cert.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +import os +import sys + +from certd_client import CertdClient + + +def require_env(name: str) -> str: + value = os.getenv(name) + if not value: + raise RuntimeError(f"Missing environment variable: {name}") + return value + + +def bool_env(name: str, default: bool = False) -> bool: + value = os.getenv(name) + if value is None or value == "": + return default + return value.lower() in ("1", "true", "yes", "y") + + +def main() -> None: + client = CertdClient( + require_env("CERTD_KEY_ID"), + require_env("CERTD_KEY_SECRET"), + base_url=os.getenv("CERTD_BASE_URL", "http://127.0.0.1:7001"), + encrypt=bool_env("CERTD_ENCRYPT"), + ) + + params = {"autoApply": bool_env("CERTD_AUTO_APPLY")} + cert_id = os.getenv("CERTD_CERT_ID") + domains = os.getenv("CERTD_DOMAINS") + cert_format = os.getenv("CERTD_FORMAT") + + if cert_id: + params["certId"] = int(cert_id) + if domains: + params["domains"] = domains + if cert_format: + params["format"] = cert_format + if "certId" not in params and "domains" not in params: + raise RuntimeError("Set CERTD_CERT_ID or CERTD_DOMAINS") + + print(client.get_cert(params)) + + +if __name__ == "__main__": + try: + main() + except Exception as exc: + print(str(exc), file=sys.stderr) + sys.exit(1)