digest.ecdsa_verify

BOOLdigest.ecdsa_verifyIDhash_methodSTRINGpublic_keySTRINGpayloadSTRINGdigestIDdigest_formatIDbase64_variant

Available inall subroutines.

Returns true if the ECDSA signature of payload using public_key matches digest. Uses the NIST P-256 curve (also known as secp256r1 or prime256v1).

Parameters

ParameterTypeDescription
hash_methodIDHash algorithm: sha256, sha384, sha512, or sha1
public_keySTRINGECDSA public key in PEM format (P-256 curve only)
payloadSTRINGThe message that was signed (raw string, not encoded)
digestSTRINGBase64-encoded signature to verify
digest_formatIDSignature encoding: der or jwt
base64_variantIDOptional. Base64 variant for decoding digest: standard, url, url_nopad (default), or default

The payload parameter is the raw message to verify, not a Base64 or hex encoding of it.

Since VCL strings cannot contain NUL bytes, binary payloads are not supported. If you need to verify a signature over binary data, you must use a text-safe encoding on both the signing and verification sides.

Supported curves

Only the NIST P-256 curve (also known as secp256r1 or prime256v1) is supported. Keys using other curves such as P-384 or P-521 will be rejected.

If an unsupported curve is detected at compile time (when the key is a literal), compilation fails with an error. If the key is loaded at runtime from a variable, the function returns false.

Signature formats

The digest_format parameter specifies how the signature is encoded before Base64 encoding.

DER format

When digest_format is der, the signature must be an ASN.1 DER-encoded ECDSA-Sig-Value structure containing the r and s integer values. This is the standard format produced by OpenSSL and most cryptographic libraries.

JWT format

When digest_format is jwt, the signature must be the raw concatenation of r and s as fixed-size 32-byte big-endian integers, as specified in RFC 7515 for JSON Web Signatures.

The jwt format only supports sha256 as the hash method, corresponding to the ES256 algorithm defined in RFC 7518. Using sha384 or sha512 with jwt format causes a compile-time error.

Base64 variants

The base64_variant parameter controls how the signature (digest parameter) is decoded:

VariantAlphabetPaddingRFC
standardA-Za-z0-9+/RequiredRFC 4648 Section 4
urlA-Za-z0-9-_RequiredRFC 4648 Section 5
url_nopadA-Za-z0-9-_OptionalRFC 4648 Section 5
defaultSame as url_nopadOptional

The default is url_nopad, which is appropriate for JWT signatures.

Examples

Verifying a DER-encoded signature

declare local var.message STRING;
declare local var.signature STRING;
declare local var.verified BOOL;
set var.message = "SECRET-MESSAGE";
set var.signature = "MEQCIFcgO8nl-TYvHYAStNZaw4UNCOxe1xdb3xlV65F_vHMiAiAF6eqlsmoCJ3dE4mnOAKlMV-FIv5jGCY3uCR02HYIbAQ";
set var.verified = digest.ecdsa_verify(sha256, {"-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs0kRbvRgu+Ej8oPCWXC1LwQDc/st
C4gWtOSMAcX7Q9L6Qn9gNVk//78zgXJ3trTQG+9fJe2C3YAVwKqqWdknzg==
-----END PUBLIC KEY-----"},
var.message,
var.signature,
der,
url_nopad);
if (var.verified) {
set req.http.X-Verified = "true";
} else {
error 403 "Invalid signature";
}

To generate your own test vectors, use OpenSSL:

$ openssl ecparam -name prime256v1 -genkey -noout -out private.pem
$ openssl ec -in private.pem -pubout -out public.pem
$ echo -n "SECRET-MESSAGE" | openssl dgst -sha256 -sign private.pem | openssl base64 -A | tr '+/' '-_' | tr -d '='

Verifying a JWT ES256 signature

A JWT consists of three Base64URL-encoded parts separated by dots: header.payload.signature. The ES256 signature is computed over the ASCII bytes of the header and payload parts joined by a dot.

This example verifies the following JWT:

eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.LhwRiF6ApB2aYbLEbGFSBxZUGwiJC2rxELwPjAIwEAIoPY-Glv_RRMt87RBm53QNds6na3eSXatVDs7HOQRuzg

Decoded, the parts are:

  • Header: {"alg":"ES256","typ":"JWT"}.
  • Payload: {"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022}.
  • Signature: 64 bytes (two 32-byte integers r and s concatenated).
declare local var.jwt STRING;
declare local var.header_payload STRING;
declare local var.signature STRING;
declare local var.verified BOOL;
# Test JWT signed with the ES256 algorithm
set var.jwt = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.LhwRiF6ApB2aYbLEbGFSBxZUGwiJC2rxELwPjAIwEAIoPY-Glv_RRMt87RBm53QNds6na3eSXatVDs7HOQRuzg";
# Extract header.payload and signature from the JWT
if (var.jwt ~ "^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$") {
set var.header_payload = re.group.1;
set var.signature = re.group.2;
} else {
error 401 "Malformed JWT";
}
# Verify the signature
set var.verified = digest.ecdsa_verify(sha256, {"-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL1fdMP0q4rFa+uTGgBds+eUjH3fH
mlfVwIhQ3yKQXL7PBzlPqLJ0eYDA9Ynq1LiNQpu6q2nu/PvID/ATE3C5Bg==
-----END PUBLIC KEY-----"},
var.header_payload,
var.signature,
jwt,
url_nopad);
if (var.verified) {
set req.http.X-JWT-Valid = "true";
} else {
error 401 "Invalid JWT signature";
}

When verifying JWTs from an Authorization: Bearer header:

declare local var.header_payload STRING;
declare local var.signature STRING;
if (req.http.Authorization ~ "^Bearer ([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$") {
set var.header_payload = re.group.1;
set var.signature = re.group.2;
} else {
error 401 "Invalid Authorization header";
}
if (!digest.ecdsa_verify(sha256, {"-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL1fdMP0q4rFa+uTGgBds+eUjH3fH
mlfVwIhQ3yKQXL7PBzlPqLJ0eYDA9Ynq1LiNQpu6q2nu/PvID/ATE3C5Bg==
-----END PUBLIC KEY-----"},
var.header_payload,
var.signature,
jwt,
url_nopad))
{
error 401 "Invalid JWT signature";
}

This example only verifies the cryptographic signature. You should also decode and validate the claims.

Using a public key from a variable

Public keys can be loaded from an edge dictionary at runtime. Always verify that the lookup succeeded before using the key:

declare local var.public_key STRING;
set var.public_key = table.lookup(es256_keys, "my-service");
if (var.public_key == "") {
error 500 "Signing key not configured";
}
if (digest.ecdsa_verify(sha256, var.public_key, req.http.Message, req.http.Sig, der)) {
set req.http.X-Signature-Valid = "true";
} else {
error 403 "Invalid signature";
}

When the key is provided at runtime, invalid keys or unsupported curves cause the function to return false rather than a compile-time error.

Algorithm security

Each signing key should be used with exactly one algorithm. Store ES256 keys separately from keys intended for other algorithms (RS256, HS256, etc.), and never select the algorithm based on untrusted input.

The alg header in a JWT is not signed and can be modified by an attacker. Never use it to select which verification function to call. Instead, determine the expected algorithm from context (the token type, the issuer, or the key identifier).

However, you can use the alg header as an early rejection path: if you expect ES256 and the token claims a different algorithm, reject it immediately without performing cryptographic verification:

declare local var.public_key STRING;
declare local var.header STRING;
declare local var.header_payload STRING;
declare local var.signature STRING;
# Look up the ES256 key for this service - only ES256 keys are stored here
set var.public_key = table.lookup(es256_keys, "auth-service");
if (var.public_key == "") {
error 500 "ES256 signing key not found";
}
# Parse the JWT into header, header.payload, and signature
if (req.http.Authorization ~ "^Bearer (([a-zA-Z0-9_-]+)\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$") {
set var.header_payload = re.group.1;
set var.header = re.group.2;
set var.signature = re.group.3;
} else {
error 401 "Invalid Authorization header";
}
# Early rejection: if the token claims a different algorithm, reject without crypto
if (digest.base64url_nopad_decode(var.header) !~ {""alg"\s*:\s*"ES256"}) {
error 401 "Unsupported algorithm";
}
# Verify with ES256
if (!digest.ecdsa_verify(sha256, var.public_key, var.header_payload, var.signature, jwt)) {
error 401 "Invalid signature";
}

Errors

This function does not set fastly.error. Invalid inputs cause the function to return false:

  • Invalid or malformed public key.
  • Unsupported elliptic curve (when key is loaded at runtime).
  • Invalid Base64 encoding in the signature.
  • Invalid signature format (malformed DER or incorrect length for JWT).
  • Signature does not match the payload.