Skip to main content

Verifying signatures

Intermediate
Tutorial

Overview

Calls made on ICP require a cryptographic signature and principal to be included within the call. This is typically attached in the form of an identity. This identity can either be authenticated or anonymous. Canisters use this identity to respond to the call and process authentication-based workflows.

An identity includes both the private and public keys. The public key is sent along with the identity encoded as a Principal and signed with a signature. When a delegation identity is sent, the same workflow is used, except a delegation chain is included as well.

For some workflows, it can be beneficial to verify the identity's signature independently from the automatic verification done on ICP as an additional layer of validation.

When a canister call is made via the HTTPS interface, the following fields are present:

  • nonce (blob, optional): Randomly generated user-provided data; can be used to create distinct calls with otherwise identical fields.

  • ingress_expiry (nat, required): An upper limit on the call's validity; expressed in nanoseconds, which avoids replay attacks since ICP will not accept calls or transition calls if their expiry date is in the past. Additionally, calls may be refused with an ingress expiry date too far in the future.

  • sender (Principal, required): The user who issued the call.

The call's authentication includes the following fields:

  • content (record): The call's content.

  • sender_pubkey (blob, optional): The public key used to authenticate the call.

  • sender_delegation (array of maps, optional): A chain of delegations that begins with one signed by the sender_pubkey and ends with the delegation to the key relating to the sender_sig; every public key should appear exactly once.

  • sender_sig (blob, optional): The signature used to authenticate the call.

The public key must authenticate the sender principal if the principal is a self-authenticating ID that is derived from that public key.

The fields sender_pubkey, sender_sig, and sender_delegation should be omitted if the sender is an anonymous principal. If the sender is authenticated, the sender_pubkey and sender_sig must be set.

The call is calculated using the content record, which allows the signature to be based on the request ID.

Transaction delegation

A transaction's signature can be delegated from one key to another. If delegation is used, the sender_delegation field contains an array of delegations with the following fields:

  • delegation (map): A map containing the fields:

    - pubkey (blob): A public key.

    - expiration (nat): The delegation's expiration, defined in nanoseconds analogously to the ingress_expiry.

    - targets (array of CanisterId, optional): Sets the delegation to apply only for requests sent to the canisters within the canister list; has a maximum of 1000 canisters.

    - senders (array of Principal, optional): Sets the delegation to only apply for requests originating from the principals in the list.

    - signature (blob): The signature for the 32-byte delegation field map, using the 27 bytes \x1Aic-request-auth-delegation as the domain separator.

The first delegation in the array has a signature created using the public key corresponding to the sender_pubkey field. All subsequent delegations are signed with the public key corresponding to the key contained in the preceding delegation.

The sender_sig field is calculated by concatenating the domain separator, \x0Aic-request, which is 11 bytes, with the 32-byte request ID, using the private key that belongs to the public key specified in the last delegation. If no delegations are present, the public key specified in sender_pubkey is used. If the delegation field is present, it should contain no more than 20 delegations.

Verifying signatures with agent

To verify a signature with an agent, an accepted identity is required. The following are accepted identity and signature types:

  • Ed25519 and ECDSA signatures.

    - Plain signatures are supported for the schemes.

  • Ed25519 or ECDSA on curve P-256 (also known as secp256r1).

    - Using SHA-256 as a hash function.

    - Using the Koblitz curve in secp256k1.

When these identities are encoded as a Principal, an agent will attach a suffix byte, which indicates whether the identity is anonymous or self-authenticating.

Self-authenticating identities using an Ed25519 or ECDSA curve will have a suffix of 2, while an anonymous identity has a single byte of 4.

Verifying signatures with the Rust ic-validator-ingress-message crate

The Rust ic-validator-ingress-message crate has been developed specifically for validating message signatures. Within this crate, the IngressMessageVerifier class containing the InternetIdentityAuthResponse can be validated as a whole using the delegation chain.

The following example displays an example of how ingress messages can be verified. This example uses a hardcoded root of trust that is set to be the NNS root public key and uses system time to derive the current time.

use ic_types::messages::{HttpCallContent, HttpRequest, SignedIngressContent};
use ic_types::Time;
use ic_validator_ingress_message::{RequestValidationError, HttpRequestVerifier, IngressMessageVerifier, TimeProvider};
fn anonymous_http_request_with_ingress_expiry(
    ingress_expiry: u64,
) -> HttpRequest<SignedIngressContent> {
    use ic_types::messages::Blob;
    use ic_types::messages::HttpCanisterUpdate;
    use ic_types::messages::HttpRequestEnvelope;
    HttpRequest::try_from(HttpRequestEnvelope::<HttpCallContent> {
        content: HttpCallContent::Call {
            update: HttpCanisterUpdate {
                canister_id: Blob(vec![42; 8]),
                method_name: "some_method".to_string(),
                arg: Blob(b"".to_vec()),
                sender: Blob(vec![0x04]),
                nonce: None,
                ingress_expiry,
            },
        },
        sender_pubkey: None,
        sender_sig: None,
        sender_delegation: None,
    })
        .expect("invalid http envelope")
}
let current_time = Time::from_nanos_since_unix_epoch(1_000);
let request = anonymous_http_request_with_ingress_expiry(current_time.as_nanos_since_unix_epoch());
let verifier = IngressMessageVerifier::default();

let result = verifier.validate_request(&request);

match result {
    Err(RequestValidationError::InvalidIngressExpiry(_)) => {}
     _ => panic!("unexpected result type {:?}", result)
}

Reference the crate's implementation logic for additional context.

Verifying signatures with the library @dfinity/standalone-sig-verifier-web

An npm library has been created as a JavaScript/TypeScript wrapper for the ic-standalone-sig-verifier Rust crate.

The following is an example of how this library can be used:

import initSigVerifier, {verifyIcSignature} from '@dfinity/standalone-sig-verifier-web';

async function example(dataRaw, signatureRaw, derPublicKey, root_key) {
    // load wasm module
    await initSigVerifier();
    try {
        // call the signature verification wasm function
        verifyIcSignature(dataRaw, signatureRaw, derPublicKey, root_key);
        console.log('signature verified successfully')
    } catch (error) {
        // the library throws an error if the signature is invalid
        console.error('signature verification failed', error)
    }
}

When using this library, you should keep the following in mind:

  • Verifying signatures on the frontend is unsafe, as malicious actors could modify the frontend code in order to bypass signature verification. Therefore, it is recommended that this library be used for demos, backends, or other situations where the code either cannot be modified or where modifications do not create a security risk.

  • This library is built from the IC master branch, meaning the API used may change.

  • This library's resulting Wasm module is large (400 KiB when gzipped); this should be taken into consideration for your project.

You can download the library.

Resources