Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

X.509

Open in ICP Ninja

🥷 Try it live — no local setup. ICP Ninja is a web-based IDE that builds and deploys this project to the mainnet for free, right in your browser. Click the badge above, or hit Deploy if you're already in Ninja. To build and run it locally instead, follow the steps below.

A minimal example canister demonstrating two use cases of X.509 certificates on the Internet Computer:

  1. How to create an X.509 certification authority (CA) certificate whose private key is a threshold signing key — the CA's private key is never revealed in cleartext and cannot be revealed due to the properties of the threshold signature protocols on the Internet Computer. This means that only the canister can sign child certificates.
  2. How to create a child certificate from a certificate signing request (CSR) provided by an external party. The CSR is generated externally (i.e. with a private key generated by the caller).

More specifically, the canister:

  • Takes a threshold key name upon initialization, e.g. (variant { Ed25519 = "test_key_1" }).
  • Generates a CA certificate via an update call to the root_ca_certificate function. The CA certificate is generated only once and then stored in the canister.
  • Generates a child certificate with a CSR provided in PEM format via an update call to the child_certificate function.
  • Uses an empty derivation path for the key that signs the root certificate.

Currently this canister only produces and accepts certificates with Ed25519 keys or ECDSA keys using curve secp256k1.

Build and deploy from the command line

Prerequisites

  • Node.js v18+
  • icp-cli: npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm
  • Rust with wasm32-unknown-unknown target: rustup target add wasm32-unknown-unknown
  • OpenSSL CLI (used by test.sh)

Install

git clone https://github.com/dfinity/examples
cd examples/rust/x509

Deploy and test

icp network start -d
icp deploy
bash test.sh
icp network stop

The canister is initialized with Ed25519 / "test_key_1" by default (works on both the local network and mainnet). To deploy with the ECDSA key or the production key:

# ECDSA with the test key
icp deploy --argument '(variant { EcdsaSecp256k1 = "test_key_1" })'

# Ed25519 with the production key on mainnet
icp deploy -e ic --argument '(variant { Ed25519 = "key_1" })'

PocketIC integration tests

The canister includes integration tests that run against a local PocketIC instance, covering Ed25519 and ECDSA secp256k1 certificate generation and verification. The openssl Rust crate used in tests requires OpenSSL development headers: on Debian/Ubuntu apt-get install libssl-dev pkg-config, on macOS it ships with Xcode Command Line Tools.

# Build the WASM first (handles platform-specific toolchain requirements)
icp build backend
# Run the integration tests
cargo test --package backend --test integration_tests

The PocketIC server binary is downloaded automatically on first run (or set POCKET_IC_BIN to an existing binary path).

Key names

Pass the key name as a plain string inside the CaKeyInformation variant:

  • "test_key_1" — mainnet test key (also works on the local network)
  • "key_1" — mainnet production key

Obtaining the root CA certificate

Using the Candid web UI

After deploying locally, the icp deploy output includes a URL to the Candid web UI where you can call root_ca_certificate and child_certificate directly.

Using the command line

icp canister call backend root_ca_certificate '()'

Example output:

(
  variant {
    Ok = record {
      x509_certificate_string = "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n";
    }
  },
)

Certificate verification with OpenSSL

Save the root CA certificate and verify it is self-signed:

icp canister call backend root_ca_certificate '()' \
  | grep 'x509_certificate_string' \
  | sed 's/.*x509_certificate_string = "//; s/"[;,]*$//; s/\\n/\n/g' \
  > root_ca_cert.pem
openssl verify -CAfile root_ca_cert.pem root_ca_cert.pem

To create a CSR and obtain a signed child certificate:

openssl genpkey -algorithm Ed25519 -out key.pem
openssl req -new -key key.pem -out request.csr -subj "/CN=Test Corporation/O=Test Inc/C=US"

CSR=$(awk '{printf "%s\\n", $0}' request.csr)
icp canister call backend child_certificate \
  "(record { pem_certificate_request = \"${CSR}\" })" \
  | grep 'x509_certificate_string' \
  | sed 's/.*x509_certificate_string = "//; s/"[;,]*$//; s/\\n/\n/g' \
  > child_cert.pem
openssl verify -CAfile root_ca_cert.pem child_cert.pem

How it works

The canister calls the schnorr_public_key or ecdsa_public_key method of the IC management canister to retrieve the public key, then calls sign_with_schnorr or sign_with_ecdsa inside the signer implementations (Ed25519Signer / EcdsaSecp256k1Signer) to produce a certificate signature. The management canister is a facade — it does not exist as a canister with isolated state; it is an ergonomic way for canisters to call IC system APIs.

Both the root CA public key and the generated certificate are cached in canister memory so that subsequent calls do not trigger additional threshold signing rounds.

The child_certificate API lets an external user generate a key pair locally, create a CSR, send it in PEM (PKCS#10) format to the canister, and receive an X.509 certificate signed by the CA key. Note that only Ed25519 and ECDSA secp256k1 keys are supported for CSRs.

Security considerations and best practices

If you base your application on this example, familiarize yourself with and adhere to the security best practices for developing on ICP. This example may not implement all the best practices.