X.509
Overview
We present a minimal example canister smart contract for showcasing two use cases of X.509. 1) How to create an X.509 certification authority (CA) certificate, where the CA certificate's private key is a threshold signing key, which 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 sample canister:
- Takes a threshold Ed25519 or ECDSA with curve
secp256k1
key name upon initialization, e.g.,(variant { Ed25519 = variant { TestKey1 } })
. - Generates a CA certificate via an update call to the
root_ca_certificate
function. The CA certificate is generated only once and afterwards stored in the smart contract. - Generates a child certificate with a CSR provided in the PEM format in an
update call to the
child_certificate
function. - Note that the derivation path of the key that is used to sign the root certificate is hard-coded to be empty.
Currently this canister only produces and accepts certificates with Ed25519
keys or ECDSA keys using curve secp256k1
.
This tutorial gives a complete overview of the development, starting with
downloading
dfx
,
up to the deployment and trying out the code on the mainnet.
Prerequisites
- Install the IC SDK v
0.23.0
or newer. - Clone the example dapp project:
git clone https://github.com/dfinity/examples
Deploy and test the canister locally
Sample code for x509-example
is provided in the examples repository, under /rust
sub-directory.
cd examples/rust/x509
dfx start --background
npm install
dfx deploy
What this does
dfx start --background
starts a local instance of the IC via the IC SDK
If successful, you should see something like this:
Deployed canisters.
URLs:
Backend canister via Candid interface:
x509_example: http://127.0.0.1:4943/?canisterId=t6rzw-2iaaa-aaaaa-aaama-cai&id=st75y-vaaaa-aaaaa-aaalq-cai
If you open the URL in a web browser, you will see a web UI that shows the
public methods the canister exposes. Since the canister exposes
root_ca_certificate
and child_certificate
methods, those are rendered in the
web UI.
Deploying the canister on the mainnet
Before deploying this canister to the mainnet, you must do the following:
- Acquire cycles (equivalent of "gas" in other blockchains). This is necessary for all canisters.
- Update the sample source code to have the right key ID. This is unique to this canister.
Step 1: Acquire cycles to deploy
Deploying to the Internet Computer requires cycles (the equivalent of "gas" on other blockchains).
Step 2: Update source code with the right key ID
To deploy the sample code, the canister needs the right key name for the right
environment. Specifically, one needs to initialize the canister with the key
name. Here, dfx deploy
will give a choice of key names.
There are three options that are supported:
dfx_test_key
: a default key ID that is used in deploying to a local version of IC (via IC SDK).test_key_1
: a master test key ID that is used in mainnet.key_1
: a master production key ID that is used in mainnet.
Note that dfx deploy
formats those name in PascalCase
instead of
snake_case
due to the formatting of types in rust
.
Step 3: Deploy to the mainnet using the IC SDK
To deploy via the mainnet, run the following commands:
npm install
dfx deploy --network ic
If successful, you should see something like this:
Deployed canisters.
URLs:
Backend canister via Candid interface:
schnorr_example_rust: https://a3gq9-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=736w4-cyaaa-aaaal-qb3wq-cai
In the example above, x509_example_rust
has the URL https://a3gq9-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=736w4-cyaaa-aaaal-qb3wq-cai and serves up the Candid web UI for this particular canister deployed on mainnet.
Obtaining the root CA certificate
Using the Candid Web UI
If you deployed your canister locally or to the mainnet, you should have a URL to the Candid web UI where you can access the public methods. We can call the root_ca_certificate
method.
In the example below, the method returns
-----BEGIN CERTIFICATE-----
MIIBxzCCAXmgAwIBAgIBADAFBgMrZXAwazELMAkGA1UEBhMCVVMxKTAnBgNVBAoM
IFdlYjMgY2VyaXRpZmNhdGlvbiBhdXRob3JpdHkgSW5jMTEwLwYDVQQDDChXZWIz
IGNlcnRpZmljYXRpb24gYXV0aG9yaXR5IGNvcnBvcmF0aW9uMB4XDTI0MDkyMDE2
MzY0MloXDTM0MDkxODE2MzY0MlowazELMAkGA1UEBhMCVVMxKTAnBgNVBAoMIFdl
YjMgY2VyaXRpZmNhdGlvbiBhdXRob3JpdHkgSW5jMTEwLwYDVQQDDChXZWIzIGNl
cnRpZmljYXRpb24gYXV0aG9yaXR5IGNvcnBvcmF0aW9uMCowBQYDK2VwAyEA8rDQ
aKVQyUr1vKqf+PzXNjg+mw3t7RPVPB9ctenyQISjQjBAMB0GA1UdDgQWBBTULvca
cvvKz89izqKDzwWZ6gwdKTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
BjAFBgMrZXADQQBlqWQ+F757rKxPXDccuhQEtrfnLoWf4rHhok/2dLioJ1+ZQda5
DNH8/kcXoPOm0jyqlVaV1ZhQm63AMwK3gSwC
-----END CERTIFICATE-----
as the root CA certificate.
(
variant {
Ok = record {
x509_certificate_string = "-----BEGIN CERTIFICATE-----\nMIIBxzCCAXmgAwIBAgIBADAFBgMrZXAwazELMAkGA1UEBhMCVVMxKTAnBgNVBAoM\nIFdlYjMgY2VyaXRpZmNhdGlvbiBhdXRob3JpdHkgSW5jMTEwLwYDVQQDDChXZWIz\nIGNlcnRpZmljYXRpb24gYXV0aG9yaXR5IGNvcnBvcmF0aW9uMB4XDTI0MDkyMDE2\nMzY0MloXDTM0MDkxODE2MzY0MlowazELMAkGA1UEBhMCVVMxKTAnBgNVBAoMIFdl\nYjMgY2VyaXRpZmNhdGlvbiBhdXRob3JpdHkgSW5jMTEwLwYDVQQDDChXZWIzIGNl\ncnRpZmljYXRpb24gYXV0aG9yaXR5IGNvcnBvcmF0aW9uMCowBQYDK2VwAyEA8rDQ\naKVQyUr1vKqf+PzXNjg+mw3t7RPVPB9ctenyQISjQjBAMB0GA1UdDgQWBBTULvca\ncvvKz89izqKDzwWZ6gwdKTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nBjAFBgMrZXADQQBlqWQ+F757rKxPXDccuhQEtrfnLoWf4rHhok/2dLioJ1+ZQda5\nDNH8/kcXoPOm0jyqlVaV1ZhQm63AMwK3gSwC\n-----END CERTIFICATE-----\n";
}
},
)
Code walkthrough
Open the file wasm_only.rs
, which will show the following Rust code that
demonstrates how to obtain a root CA certificate.
Obtaining root CA certificate
#[update]
async fn root_ca_certificate() -> Result<X509CertificateString, String> {
// if the certificate is already cached, return it
if let Some(certificate) = ROOT_CA_CERTIFICATE_PEM.with(|inner| inner.get().map(|v| v.clone()))
{
return Ok(X509CertificateString {
x509_certificate_string: certificate,
});
}
// if the certificate is not cached, create it and try to cache it
let serial_number = SerialNumber::from(0u32);
let subject = Name::from_str(
"CN=DEMO Web3 certification authority corporation,O=DEMO Web3 ceritifcation authority Inc,C=US",
)
.unwrap();
let newly_constructed_x509_certificate_string = match CA_KEY_INFORMATION
.with(|value| *value.borrow())
{
CaKeyInformation::Ed25519(_) => {
let signer = Ed25519Signer::new()
.await
.map_err(|e| format!("failed to create Ed25519 signer: {e:?}"))?;
let subject_public_key =
der::asn1::BitString::new(0, root_ca_public_key_bytes().await?)
.map_err(|e| format!("source: {:?}", e.source()))?;
let pub_key = SubjectPublicKeyInfoOwned {
algorithm: signer.signature_algorithm_identifier().unwrap(),
subject_public_key,
};
pem_certificate_signed_by_root_ca(
Profile::Root,
serial_number,
validity(),
subject,
pub_key,
signer,
)
.await
.map_err(|e| format!("failed to create root certificate: {e:?}"))?
}
CaKeyInformation::EcdsaSecp256k1(_) => {
let signer = EcdsaSecp256k1Signer::new()
.await
.map_err(|e| format!("failed to create ECDSA secp256k1 signer: {e:?}"))?;
let public_key_bytes_compressed = root_ca_public_key_bytes().await?;
let public_key_bytes_uncompressed =
k256::ecdsa::VerifyingKey::from_sec1_bytes(public_key_bytes_compressed.as_slice())
.map_err(|e| format!("malformed public key: {e:?}"))?
.to_encoded_point(false);
let subject_public_key =
der::asn1::BitString::new(0, public_key_bytes_uncompressed.as_bytes())
.map_err(|e| format!("source: {:?}", e.source()))?;
let pub_key = SubjectPublicKeyInfoOwned {
algorithm: AlgorithmIdentifier {
// Public Key Algorithm: id-ecPublicKey (1.2.840.10045.2.1)
oid: pkcs8::ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"),
parameters: Some(der::Any::from(k256::Secp256k1::OID)),
},
subject_public_key,
};
pem_certificate_signed_by_root_ca(
Profile::Root,
serial_number,
validity(),
subject,
pub_key,
signer,
)
.await
.map_err(|e| format!("failed to create root certificate: {e:?}"))?
}
};
let x509_certificate_string = ROOT_CA_CERTIFICATE_PEM.with(move |inner| {
inner
.get_or_init(|| newly_constructed_x509_certificate_string)
.clone()
});
Ok(X509CertificateString {
x509_certificate_string,
})
}
async fn pem_certificate_signed_by_root_ca<Signer>(
profile: Profile,
serial_number: SerialNumber,
validity: Validity,
subject: Name,
subject_public_key_info: SubjectPublicKeyInfoOwned,
signer: Signer,
) -> Result<String, String>
where
Signer: Sign,
Signer: Keypair + DynSignatureAlgorithmIdentifier,
Signer::VerifyingKey: EncodePublicKey,
{
let mut builder = CertificateBuilder::new(
profile,
serial_number,
validity,
subject,
subject_public_key_info,
&signer,
)
.expect("Create certificate");
let blob = builder
.finalize()
.map_err(|e| format!("failed to finalize certificate builder: {e:?}"))?;
let signature = BitString::from_bytes(&signer.sign(&blob).await?)
.map_err(|e| format!("wrong signature length: {e:?}"))?;
let certificate = builder
.assemble(signature)
.map_err(|e| format!("failed to assemble certificate: {e:?}"))?;
certificate
.to_pem(LineEnding::LF)
.map_err(|e| format!("failed to encode certificate: {e:?}"))
}
In the code above, the canister calls root_ca_public_key_bytes
which calls
schnorr_public_key
method or ecdsa_public_key
method of the IC management
canister
(aaaaa-aa
). Then, inside pem_certificated_signed_by_root_ca
, the canister
calls sign_with_schnorr
method or sign_with_ecdsa
method of the IC
management canister inside Ed25519Signer::sign
or
EcdsaSecp256k1Signer::sign
, respectively, in order to produce a certificate
signature. More details about Ed25519Signer
and EcdsaSecp256k1Signer
can be
found in wasm_only/signer.rs
.
The IC management
canister
is just a facade; it does not exist as a canister (with isolated state, Wasm
code, etc.). It is an ergonomic way for canisters to call the system API of the
IC (as if it were a single canister). In the code below, we use the management
canister to create a Schnorr public key. Canister ID "aaaaa-aa"
declares the IC management canister in the canister code.
Creating a certificate signing request (CSR) and obtaining the signed child certificate
#[update]
async fn child_certificate(
certificate_request_info: PemCertificateRequest,
) -> Result<X509CertificateString, String> {
let cert_req =
CertReq::from_pem(certificate_request_info.pem_certificate_request.as_bytes())
.map_err(|e| format!("failed to parse PEM certificate signing request: {e:?}"))?;
verify_certificate_request_signature(&cert_req)?;
if !cert_req.info.attributes.is_empty() {
return Err("Attributes are currently not supported in this example".to_string());
}
prove_ownership(&cert_req, ic_cdk::api::caller() /*, ... */)?;
let root_certificate_pem = root_ca_certificate().await?;
let root_certificate =
x509_cert::Certificate::from_pem(root_certificate_pem.x509_certificate_string.as_str())
.map_err(|e| format!("failed to parse PEM root CA certificate: {e:?}"))?;
let profile = Profile::Leaf {
issuer: root_certificate.tbs_certificate.subject.clone(),
enable_key_agreement: false,
enable_key_encipherment: false,
};
let serial_number = SerialNumber::from(next_child_certificate_serial_number());
// For simplicity of this example, let's just use the same validity
// period as the root certificate. In a real application, the validity
// would normally not start in the past and might end well before the root
// certificate validity ends. Also, the validity of the child
// ceritifcate should always be in the time frame of the root
// certificate's validity.
let validity = root_certificate.tbs_certificate.validity.clone();
let x509_certificate_string = {
match CA_KEY_INFORMATION.with(|value| *value.borrow()) {
CaKeyInformation::Ed25519(_) => {
let signer = Ed25519Signer::new()
.await
.map_err(|e| format!("failed to create Ed25519 signer: {e:?}"))?;
pem_certificate_signed_by_root_ca(
profile,
serial_number,
validity,
cert_req.info.subject.clone(),
cert_req.info.public_key.clone(),
signer,
)
.await
.map_err(|e| format!("failed to create child certificate: {e:?}"))?
}
CaKeyInformation::EcdsaSecp256k1(_) => {
let signer = EcdsaSecp256k1Signer::new()
.await
.map_err(|e| format!("failed to create Ed25519 signer: {e:?}"))?;
pem_certificate_signed_by_root_ca(
profile,
serial_number,
validity,
cert_req.info.subject.clone(),
cert_req.info.public_key.clone(),
signer,
)
.await
.map_err(|e| format!("failed to create child certificate: {e:?}"))?
}
}
};
Ok(X509CertificateString {
x509_certificate_string,
})
}
Similarly to the root CA certificate, child certificates are signed via the
pem_certificate_signed_by_root_ca
function. The major difference here is that
the meta-information about the certificate such as subject and public key
information are provided via a
CSR in the PKCS#10
format, which is the most widely-used CSR format and can be generate e.g. using
OpenSSL.
The child_certificate
API allows for an external user to generate a key pair
locally, create a CSR, send it in the PEM format to the smart contract, and get
a X.509 certificate for the CSR, signed with the key of the CA certificate.
Note that, similarly to CA certificates, currently only Ed25519 and ECDSA with
curve secp256k1
are supported for child certificates, i.e., also for CSRs.
Certificate verification
Certificates obtained from the smart contract can be validated e.g. using OpenSSL.
To validate the root CA certificate from a locally deployed smart contract, the following command can be used.
dfx canister call x509_example_rust root_ca_certificate --output json | jq '.["Ok"].["x509_certificate_string"]' | sed -e 's/\\n/\n/g' -e 's/\"//g' > root_ca_cert.pem && openssl verify -CAfile root_ca_cert.pem root_ca_cert.pem
To create a CSR, the following script could be used.
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"
To validate a child certificate, the following command could be used.
CSR=$(cat request.csr)
dfx canister call x509_example_rust child_certificate "(record { pem_certificate_request = \"$CSR\"; } )" --output json | jq '.["Ok"].["x509_certificate_string"]' | sed -e 's/\\n/\n/g' -e 's/\"//g' > child_cert.pem &&
openssl verify -CAfile root_ca_cert.pem child_cert.pem
Conclusion
In this walkthrough, we deployed a sample smart contract that:
- Generated a root CA certificated self-signed with private Ed25519 keys even though canisters do not hold Schnorr keys themselves.
- Requested child certificate signed with the private Ed25519 key of the root CA certificate.