{"id":23730478,"url":"https://github.com/markrad/certserver","last_synced_at":"2026-02-19T14:30:18.152Z","repository":{"id":173091525,"uuid":"648432242","full_name":"markrad/CertServer","owner":"markrad","description":"Insecure management of test X.509 certificates","archived":false,"fork":false,"pushed_at":"2024-05-22T15:26:40.000Z","size":1040,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-05-22T16:42:57.596Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/markrad.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-06-02T00:52:52.000Z","updated_at":"2024-05-30T00:22:50.847Z","dependencies_parsed_at":"2023-12-01T22:27:54.696Z","dependency_job_id":"92c08ee4-2624-4eac-9106-2dd7f809d30c","html_url":"https://github.com/markrad/CertServer","commit_stats":null,"previous_names":["markrad/certserver"],"tags_count":54,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markrad%2FCertServer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markrad%2FCertServer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markrad%2FCertServer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markrad%2FCertServer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/markrad","download_url":"https://codeload.github.com/markrad/CertServer/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239841730,"owners_count":19705980,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-31T02:38:43.006Z","updated_at":"2026-02-19T14:30:18.108Z","avatar_url":"https://github.com/markrad.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CertServer\n\n## Description\nThis is a simple website that is designed to help manage certificates and private keys. With that said please note **this is only intended for test scenarios with certificates and keys that are only used for testing. DO NOT use this for production certificates and keys. It is NOT SECURE.** If in doubt, then read the [LICENSE](./LICENSE).\n\n## Justification\nTypically, my job will require me to generate certificates for various purposes, self-signed certificate authorities, intermediates, and leaf certificates. Many of these are for X.509 authentication on test systems, as in, I don't care if you break into it because there is nothing useful.\n\nThe problem I would run into was tracking various certificates over a multitude of different machines for different purposes. Often I would end up using OpenSSL to regenerate them. I don't know about you, but I have to look up the OpenSSL arguments every time I need to do this.\n## Usage\nMy solution to this is this simple web application. You can create a root certificate, from that create a chain of intermediate certificates, and finally a leaf certificate. Once they have all been created, you can download the leaf certificate's private key and the full certificate chain from the leaf up.\n\nIn my case, these certificates are typically used for X.509 authentication with an Azure Device Provisioning Service or an Azure IoT hub. These have been tested and work as expected. Also tested is generating a root and intermediate for use in an nested IoT Edge parent/child relationships. These also replace the Edge quick start certificates too.\n\nYou can also generate a root and a leaf and use it for protecting a website with TLS. The common name will need to match the fully qualified domain name of the server for this to work. Subject alternative names can also be added by IP or alternative DNS name.\n\nYou can also upload certificates and keys to it and it will determine if the new files have any relationship to the existing files such as one certificate being signed by another or a key being the pair to a certificate.\n\nThe webpage itself is fairly crude. I am not a web or even UI person. \n## Running the Server\nOnce you have cloned this GitHub repository, the main script takes just one optional argument which is the path to a configuration file. This is expected to be in yaml format and it will need to have an extension of yml, due to a limitation of the yaml parsing library I used. You start it thus:\n`node output/index.js ./config.yml`  \nThere is a sample config file [here](./config_sample.yml). This are the settings that will be used if no config.yml is passed. It looks like this:\n### The Default config File\n```yaml\ncertServer:\n  root: \"./data\"\n  port: 4141\n  certificate: null\n  key: null\n  subject:\n    C: US\n    ST: Washington\n    L: Redmond\n    O: None\n    OU: None\n``` \n### Config file options\n- **certServer:** \nThis is required  \n  -  **root:** \nThis specifies the root directory that the server will use to save certificates, private keys, and its database. Defaults to *./data*  \n  - **port:** The port the webserver will listen on. Defaults to *4141*. \n  - **certificate:** When specified with a matching key, it will run the server in SSL mode.\n  - **key:** Key for certificate above.\n  - **subject:** If you want subject defaults, this is required  \n    + **C:** Default for subject country  \n    + **ST:** Default for subject state  \n    + **L:** Default for subject location (city)  \n    + **O:** Default for subject organization  \n    + **OU:** Default for subject organizational unit  \n## Running in a Docker Container\nA [dockerfile](./docker/Dockerfile) is provided that will build an image to run the server in Linux Alpine and a [docker-compose](./docker/docker-compose.yml) file that show how you might run it using mounted volumes for the data and the config. It is recommended that you mount the directory that you specified as the root directory in the config and the directory that contains the config file itself.\n\nThe easier option is to pull the image from the ghcr repository. You can do this with latest or a specific version number (M.m.p such as 1.2.11):\n```\ndocker pull ghcr.io/markrad/certserver:latest\n```\nGo to [the packages](https://github.com/markrad/CertServer/pkgs/container/certserver) page to see available versions.\nOnce you have the image you can either use the docker compose file mentioned above, or run it with:\n```\ndocker run \\\n -p 4141:4141 \\\n -v /some/path/config.yml:/config/config.yml \n -v /some/path/data:/path/specified/in/config\n```\n## Code Samples\nCode samples to connect to an IoT hub or a DPS with self-signed or CA authentication are provided [here](./devicesamples/). Further documentation for these samples and setting up IoT Edge certificates can be found [here](./Examples.md).\n \n## REST API\n\nA REST API is also available since your host may not be capable of running a web browser. In most cases you can use the name of the certificate or key, but, since they are not guaranteed to be unique, you can also use the id displayed next to each certificate and key.\n\n### Helper method\nThis method will (attempt) to return a script that you can utilize for acquiring certificates, certificate chains, and keys. If you are on Windows it will return a ps1 script and on Linux it will return a bash script. \n`POST http://server:4141/api/helper[?os=linux|windows|mac]`\n\nThe os parameter is optional. The server will attempt to determine the appropriate operating system from the user agent. If this is wrong or unsupported it can be overridden by specifying the required script.\n\nThis call will return a script which should be saved to your storage. For example on Linux or Mac:\n```bash\ncurl http://server:4141/api/helper -o helper.sh\nsource helper.sh\n# Get the certificate with id \u003cid\u003e\ngetcert \u003cid\u003e\n# Get the key with id \u003cid\u003e\ngetkey \u003cid\u003e\n# Get the certificate chain starting at id \u003cid\u003e\ngetchain \u003cid\u003e\n```\nor on Windows\n```powershell\nInvoke-WebRequest -Uri http://server:4141/api/helper -OutFile helper.ps1\n. helper.ps1\n# Get the certificate with id \u003cid\u003e\nGet-CertPem \u003cid\u003e\n# Get the key with id \u003cid\u003e\nGet-KeyPem \u003cid\u003e\n# Get the certificate chain starting at id \u003cid\u003e\nGet-Chain \u003cid\u003e\n```\n\n### Create a new self-signed certificate authority (root CA)\n`POST http://server:4141/api/createcacert`  \nPost data:\n```JSON\n{\n    \"country\": \"optional country\",\n    \"state\": \"optional state\",\n    \"location\": \"optional location\",\n    \"organization\": \"optional organization\",\n    \"unit\": \"optional unit\",\n    \"commonName\": \"required common name\",\n    \"validFrom\": \"required date from in format yyyy/dd/dd\",\n    \"validTo\": \"required date to in format yyyy/dd/dd\"\n}\n\n```\nSample response:\n```JSON\n{\n    \"message\": \"Certificate/Key someName/someName_key added\",\n    \"ids\": {\n      \"certificateId\": \u003cid\u003e,\n      \"keyId\": \u003cid\u003e\n    }\n}\n```\n#### Examples\n##### Curl\n```bash\ncurl -X POST -H 'Content-type: application' --data '\n{\n  \"country\": \"US\",\n  \"state\": \"WA\",\n  \"location\": \"anyCity\",\n  \"organization\": \"myCompany\",\n  \"unit\": \"three\",\n  \"commonName\": \"test name\",\n  \"validFrom\": \"2024\\01\\01\"\n  \"validTo\": \"2028\\01\\01\"\n}'  http://myserver:4141/api/createcacert\n```\n### Create a new intermediate certificate\nIntermediate certificates can be signed by either a root CA or another intermediate certificate.  \n`POST http://server:4141/api/createintermediatecert`  \nPost data:\n```JSON\n{\n    \"country\": \"optional country\",\n    \"state\": \"optional state\",\n    \"location\": \"optional location\",\n    \"organization\": \"optional organization\",\n    \"unit\": \"optional unit\",\n    \"commonName\": \"required common name\",\n    \"validFrom\": \"required date from in format yyyy/dd/dd\",\n    \"validTo\": \"required date to in format yyyy/dd/dd\",\n    \"signer\": \"id of certificate to sign this certificate\",\n    \"password\": \"password for signer's key if required\"\n}\n\n```\nSample response:\n```JSON\n{\n    \"message\": \"Certificate/Key intName/intName_key added\",\n    \"ids\": {\n      \"certificateId\": \u003cid\u003e,\n      \"keyId\": \u003cid\u003e\n    }\n}\n```\n#### Examples\n##### Curl\n```bash\ncurl -X POST -H 'Content-type: application' --data '\n{\n  \"country\": \"US\",\n  \"state\": \"WA\",\n  \"location\": \"anyCity\",\n  \"organization\": \"myCompany\",\n  \"unit\": \"three\",\n  \"commonName\": \"test name\",\n  \"validFrom\": \"2024\\01\\01\"\n  \"validTo\": \"2028\\01\\01\",\n  \"signer\": \"15\",\n  \"password\": \"secret-p@ssword\"\n}'  http://myserver:4141/api/createintermediatecert\n```\n### Create a new leaf certificate\nLeaf certificates can be signed by either a root CA or intermediate certificate but they **cannot sign** other certificates.  \n`POST http://server:4141/api/createleafcert`  \nPost data:\n```JSON\n{\n    \"country\": \"optional country\",\n    \"state\": \"optional state\",\n    \"location\": \"optional location\",\n    \"organization\": \"optional organization\",\n    \"unit\": \"optional unit\",\n    \"commonName\": \"required common name\",\n    \"validFrom\": \"required date from in format yyyy/dd/dd\",\n    \"validTo\": \"required date to in format yyyy/dd/dd\",\n    \"signer\": \"id of certificate to sign this certificate\",\n    \"password\": \"password for signer's key if required\",\n    \"SANArray\": [\n      \"DNS: a string for an alternative name such as localhost\",\n      \"IP: an IP or IPv6 address in standard representation\"\n    ]\n}\n\n```\n_Note, in the post data, the SANArray entries must begin with the string 'DNS: ' or 'IP: '. Anything else will be ignored._  \nSample response:\n```JSON\n{\n    \"message\": \"Certificate/Key leafName/leafName_key added\",\n    \"ids\": {\n      \"certificateId\": \u003cid\u003e,\n      \"keyId\": \u003cid\u003e\n    }\n}\n```\n#### Examples\n##### Curl\n```bash\ncurl -X POST -H 'Content-type: application' --data '\n{\n  \"country\": \"US\",\n  \"state\": \"WA\",\n  \"location\": \"anyCity\",\n  \"organization\": \"myCompany\",\n  \"unit\": \"three\",\n  \"commonName\": \"test name\",\n  \"validFrom\": \"2024\\01\\01\"\n  \"validTo\": \"2028\\01\\01\",\n  \"signer\": \"15\",\n  \"password\": \"secret-p@ssword\",\n  \"SANArray\": [\n    \"DNS:mysite.com\",\n    \"IP:222.33.22.3\"\n  ]\n}'  http://myserver:4141/api/createleafcert\n```\n### Get a list of certificates by type:  \n`GET http://server:4141/api/certlist?type=root | intermediate | leaf | key`  \nSample response:  \n```json\n{\n  \"files\": [\n    {\n      \"name\": \"A_Root\",\n      \"type\": \"root\",\n      \"id\": 1\n    }\n  ]\n}\n```\n#### Examples\n##### Curl\n```bash\ncurl http://myserver:4141/api/certlist?type=leaf\n```\n##### PowerShell\n```powershell\nInvoke-WebRequest -Uri http://myserver:4141/api/certlist?type=leaf\n```\n### Download a certificate pem file:  \n`GET http://server:4141/api/getcertificatepem?id=\u003ccertificate id\u003e` or  \n`GET http://server:4141/api/getcertificatepem?name=\u003ccertificate name\u003e`  \nReturns the pem file. If name is used and two certificates share the same common name it will fail with an error.\n### Download the certificate's full chain file:\n`GET http://server:4141/api/chaindownload?id=\u003ccertificate id\u003e` or  \n`GET http://server:4141/api/chaindownload?name=\u003ccertificate name\u003e`  \nReturns a pem file containing the full chain of certificates from the one selected up. This is in the correct order to pass as a full chain pem file. If name is used and two certificates share the same common name it will fail with an error.\n### Upload a certificate pem file:  \n`POST http://server:4141/api/uploadcert`  \nUploads an existing certificate to the server. The pem string is placed in the post data. The POST must follow the following conventions:  \n  + The pem content is in standard 64 byte lines. Hint: use --data-binary @filename when using curl\n  + The Content-Type header must be set to text/plain. In curl -H \"Content-Type: text/plain\"  \nSample response:\n```json\n{\n  \"message\":\"Certificate Baltimore_CyberTrust_Root of type root added\",\n    \"ids\": {\n      \"certificateId\": \u003cid\u003e\n    }\n}\n```\n##### Curl\n```\ncurl -X POST -H \"Content-Type: text/plain\" --data-binary @./mycert.pem http://myserver:4141/api/uploadcert \n```\n##### PowerShell\n```powershell\n$body = [System.IO.File]::ReadAllText('.\\mycert.pem')\nInvoke-WebRequest -Uri http://myserver:4141/api/uploadcert `\n  -ContentType 'text/plain' `\n  -Method POST `\n  -Body $body\n```\n### Get a certificate's details\nReturns the pertinent details of a specific certificate including the tags.  \n`GET http://server:4141/api/certDetails?id=\u003ccertificate id\u003e` or  \n`GET http://server:4141/api/certDetails?name=\u003ccertificate name\u003e`  \nSample response:\n```json\n{\n  \"id\": 128,\n  \"certType\": \"root\",\n  \"name\": \"test name\",\n  \"issuer\": {\n    \"C\": \"US\",\n    \"ST\": \"WA\",\n    \"L\": \"anyCity\",\n    \"O\": \"myCompany\",\n    \"OU\": \"three\",\n    \"CN\": \"test name\"\n  },\n  \"subject\": {\n    \"C\": \"US\",\n    \"ST\": \"WA\",\n    \"L\": \"anyCity\",\n    \"O\": \"myCompany\",\n    \"OU\": \"three\",\n    \"CN\": \"test name\"\n  },\n  \"validFrom\": \"2024-01-01T08:00:00.000Z\",\n  \"validTo\": \"2028-01-01T08:00:00.000Z\",\n  \"serialNumber\": \"18:2b:df:a7:97:d7:10:d5:f0:e6:b9:92:b9:1d:40:63:1a:81:a0:65\",\n  \"signer\": \"test name\",\n  \"signerId\": 128,\n  \"keyId\": 121,\n  \"fingerprint\": \"1F:C8:6B:58:A8:5E:EB:57:56:E2:F3:14:09:C0:52:D4:84:FF:11:00\",\n  \"fingerprint256\": \"B6:88:4E:B2:81:44:DE:0D:89:CE:AE:47:E1:01:CE:E5:2B:16:A3:E5:89:63:17:CE:31:6C:65:C6:E7:38:8C:CD\",\n  \"signed\": [\n    127,\n    128\n  ],\n  \"tags\": [\n    \"tag 2\",\n    \"tag 3\",\n    \"tag 1\"\n  ]\n}\n```\n### Update a certificate's tags\n__Replace__ the tags associated with the certificate. Note the tags passed will replace all of the tags currently associated with the certificate.  \n`POST http://server:4141/api/updateCertTag?id=\u003ccertificate id\u003e` or  \n`POST http://server:4141/api/updateCertTag?name=\u003ccertificate name\u003e`  \nPost data:\n```json\n{\n  \"tags\": [ \"tag-a\", \"tag-b\" ]\n}\n```\nSample response:\n```json\n{\n  \"message\": \"Certificate tags updated\"\n}\n```\n### Delete a certificate:  \n`DELETE http://server:4141/api/deleteCert?id=\u003ccertificate id\u003e` or  \n`DELETE http://server:4141/api/deleteCert?name=\u003ccertificate name\u003e`  \nDeletes the certificate from the server. If name is used and two certificates share the same common name it will fail with an error.  \nSample response:\n```json\n{\n  \"message\": \"Certificate deleted\"\n}\n```\n#### Examples\n##### Curl\n```bash\ncurl -X DELETE http://myserver:4141/api/deleteCert?id=33\n```\n### Get a list of keys:  \n`GET http://server:4141/api/keylist`  \nSample response:  \n```json\n{\n  \"files\": [\n    {\n      \"name\": \"A_Key\",\n      \"type\": \"key\",\n      \"id\": 1\n    }\n  ]\n}\n```\n### Download a key pem file:  \n`GET http://server:4141/api/getkeypem?id=\u003ckey id\u003e` or  \n`GET http://server:4141/api/getkeypem?name=\u003ckey name\u003e`  \nReturns the pem file. If name is used and two keys share the same common name it will fail with an error.\n### Upload a key pem file:  \n`POST http://server:4141/api/uploadkey`  \nUploads an existing key to the server. The pem data is placed in the post data. The POST must follow the following conventions:  \n  + The pem content is in standard 64 byte lines. Hint: use --data-binary @filename when using curl\n  + The Content-Type header must be set to text/plain. In curl -H \"Content-Type: text/plain\"  \nSample response:\n```json\n{\n    \"message\": \"Key intName_key added\",\n    \"ids\": {\n      \"keyId\": \u003cid\u003e\n    }\n}\n```\n#### Examples\n##### Curl\n```bash\ncurl -X POST -H \"Content-Type: text/plain\" --data-binary @./mykey.pem http://myserver:4141/api/uploadkey \n```\n##### PowerShell\n```powershell\n$body = [System.IO.File]::ReadAllText('.\\mykey.pem')\nInvoke-WebRequest -Uri http://myserver:4141/api/uploadkey `\n  -ContentType 'text/plain' `\n  -Method POST `\n  -Body $body\n```\n### Delete a key:  \n`DELETE http://server:4141/api/deletekey?id=\u003ckey id\u003e` or  \n`DELETE http://server:4141/api/deletekey?name=\u003ckey name\u003e`  \nDeletes the key from the server. If name is used and two certificates share the same common name it will fail with an error.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkrad%2Fcertserver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarkrad%2Fcertserver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkrad%2Fcertserver/lists"}