Certificate Signatures: How to Verify an Assurd Certificate Yourself

|By Assurd Engineering Lab|The Playbook

A certificate is only worth what it costs to forge. If anyone with a text editor could change a grade or a hotspot delta and still show a page that looks like an Assurd certificate, the measurements behind it would not matter. So every certificate minted since KMS signing went live carries a digital signature. Anyone can check it, without an account and without trusting this website.

This post explains what the signature covers and how it is made (SOP-GPU-001 v1.1, Step 5.2). It shows how to verify one yourself, and is just as explicit about what a valid signature does not prove.

What is signed

At mint, the whole certificate document is signed, except the signed_envelope field that will hold the signature. That covers:

  • every pillar of the certificate: physical triage, silicon identity, thermals, performance and the grade;
  • the report_id, the asset_id and the time of issue, generated_at;
  • the SOP version the card was tested under;
  • the legacy fingerprint, certificate_signature.

Change any single byte of any of these and the signature no longer verifies.

Canonical JSON

A signature covers bytes, but a certificate is a JSON value, and one value can be written as many different byte strings. The backend is written in Python. Python's default JSON writer produces

{"score": 72.0, "grade": "GOLD", "asset_id": "a1"}

A JavaScript program holding the same value would write 72, not 72.0, and might order the keys differently. Same certificate, different bytes, different hash: a signature over one would fail to verify against the other.

The fix is to agree on exactly one byte string per value. Assurd uses the JSON Canonicalization Scheme (JCS, RFC 8785):

  • object keys are sorted;
  • there is no insignificant whitespace;
  • numbers are written in their shortest round-trip form;
  • strings use a minimal, fixed escaping.

The value above becomes

{"asset_id":"a1","grade":"GOLD","score":72}

There is one more rule, and it matters more than the scheme: verify the bytes, never re-serialise them. The signed bytes are stored base64-encoded in the envelope. A verifier decodes them and checks the signature over exactly those bytes. The certificate you read is then the parse of those verified bytes. Nothing is ever re-encoded on the way, so no JSON library anywhere can break or bend the check.

The ES256 signature

The signing key is an ECDSA key on the P-256 curve, used with SHA-256: the algorithm called ES256. It lives in Google Cloud KMS, in key ring assurd-signing, key certificate-signing. The private key never leaves KMS: no technician, lab or Assurd service can read it. KMS will only sign a digest it is given.

With cc the certificate, kk the private key and σ\sigma the signature:

σ=Signk(h),h=SHA256(JCS(c)).\begin{gathered} \sigma = \mathrm{Sign}_{k}\bigl(h\bigr), \\ h = \mathrm{SHA256}\bigl(\mathrm{JCS}(c)\bigr). \end{gathered}

An ECDSA signature is a pair of 256-bit integers (r,s)(r, s). Assurd stores it in the fixed P1363 form: rr and ss as 32 bytes each, concatenated, 64 bytes in all.

Before anything is stored, the backend verifies the new signature against the KMS public key. If KMS is unavailable or the check fails, no certificate is minted. An unsigned certificate is never issued as a fallback.

The signed envelope

FieldContent
payload_b64Base64 of the exact RFC 8785 bytes that were signed
signature_b64Base64 of the 64-byte P1363 signature (r∥s)(r \Vert s)
alg / sig_formatES256 / P1363
kms_key_versionFull KMS resource name of the key version that signed
signed_atEquals the certificate's generated_at
legacy_signatureCopy of certificate_signature

The certificate stored in the public registry is exactly the decoded payload plus this envelope.

Keys and rotation

The public half of each signing key is published as a JSON Web Key set at

https://api.assurd.in/api/registry/keys

Each key's kid is the full KMS key-version name, so the envelope's kms_key_version says exactly which key to use. The production key versions are also pinned in the website's build, so the certificate page has its own copy and does not depend on the live registry.

Keys are rotated by creating a new key version. Old versions are never disabled or destroyed, because every certificate they signed must stay verifiable for its whole life.

Verification in the browser

Every certificate page runs the check itself, in your browser:

  1. Select the key whose kid equals the envelope's kms_key_version: the pinned copy first, then the live registry.
  2. Base64-decode the payload and the signature.
  3. Verify the ES256 signature over the decoded payload bytes, exactly as decoded.
  4. Parse the payload and check that its report_id is the certificate you asked for. A valid signature on a different certificate is not a pass.

The page then shows one of four results:

  • Signature verified. The certificate shown is the verified payload. Any field shown elsewhere that differs from it is not certified.
  • Signature invalid. The certificate body is hidden.
  • Signing key not recognised. Neither the pinned keys nor the registry hold the named key version.
  • Legacy · not KMS-signed. The certificate was minted before KMS signing and has no envelope (see The fingerprint is not a signature).

Verify it offline

A check that runs on our website still asks you to trust our website's code. The certificate page's Verify offline dialog removes that. It downloads two files:

  • the envelope, as assurd-<report-id>-envelope.json;
  • the public key that verified it, as assurd-signing-key.pem.

It then gives you a command to run in the download folder (bash, jq, xxd, OpenSSL 3). The command is shown here with one step per line:

E='assurd-<report-id>-envelope.json' K='assurd-signing-key.pem';
jq -r .payload_b64 "$E" | base64 -d > payload.json &&
S=$(jq -r .signature_b64 "$E" | base64 -d | xxd -p -c64) &&
printf 'asn1=SEQUENCE:s\n[s]\nr=INTEGER:0x%s\ns=INTEGER:0x%s\n' "${S:0:64}" "${S:64:64}" > sig.cnf &&
openssl asn1parse -genconf sig.cnf -out sig.der -noout &&
openssl dgst -sha256 -verify "$K" -signature sig.der payload.json

Line by line, it does the following:

  1. It writes the signed bytes to payload.json, unchanged. That file is the certificate as signed: open it and read it.
  2. It reads the 64-byte signature as hex, then takes the first 32 bytes as rr and the last 32 as ss.
  3. It rebuilds the signature in the DER form OpenSSL expects. DER encodes SEQUENCE { INTEGER r, INTEGER s }, and an integer whose top bit is set gets a leading zero byte. So a 64-byte P1363 signature becomes 70, 71 or 72 bytes of DER, depending on whether neither, one or both of rr and ss need that byte. It is shorter only in the rare case that rr or ss starts with zero bytes.
  4. It verifies. OpenSSL prints Verified OK for an intact certificate, and Verification failure if a single byte of the payload or signature differs.

Checking the key itself

The offline check proves that payload.json was signed by the key in assurd-signing-key.pem. That the PEM is Assurd's key is a separate question, because the PEM also came from this website. To answer it independently, compare the PEM with the published key set:

openssl ec -pubin -in assurd-signing-key.pem -text -noout

This prints the public point as 04 followed by the 32-byte xx and 32-byte yy coordinates, in hex. The registry entry with the same kid gives the same xx and yy, base64url-encoded. If they match, the certificate was signed by the key Assurd publishes for that version.

The fingerprint is not a signature

Every certificate also carries certificate_signature. Despite its name, it is a content fingerprint, not a signature:

"SHA256:"∥HEX(SHA256(id ∣ asset ∣ rom ∣ t)),\mathtt{"SHA256:"} \Vert \mathrm{HEX}\bigl(\mathrm{SHA256}( \mathit{id} \,|\, \mathit{asset} \,|\, \mathit{rom} \,|\, t)\bigr),

with the four fields joined by | and the digest written in uppercase hex. It uses no key, so anyone can compute it, including a forger. You can reproduce the sample certificate's fingerprint yourself from four of its own fields:

printf '%s' 'sample|sample-asset|0de3b0a12d41a594b860ef22709934d14f99da1e09e1ee1f4d15e53fe5518312|2026-11-01T06:30:00+00:00' | sha256sum

It prints

b174712ebbc06779d3ffb0ecce5c506c33295c44c1a52187502cfcf6b2bd096c

which is the sample's SHA256:B174712E… in uppercase.

Being able to recompute it is exactly why it carries no authority. Certificates minted before KMS signing have only this fingerprint and no envelope. Their pages say Legacy · not KMS-signed, never "verified".

Test vector

JCS turns the Python output with key order score, grade, asset_id and the value 72.0 into the bytes with sorted keys and 72. The sample certificate's fingerprint input hashes to B174712E…096C, uppercase.

What a signature proves, and what it does not

A verified signature proves two things:

  • Integrity. The certificate you are reading is byte-for-byte the one that was signed. Not one field, digit or flag has changed since mint.
  • Origin. It was signed by the Assurd KMS key version it names, which no lab or person can extract.

It does not prove:

  • That the measurements are right. The signature fixes what the lab recorded and the Rules Engine computed. It does not re-measure the card. The quality of the measurements rests on the procedure, which the other posts in this series explain.
  • That the card in front of you is the certified card. The certificate names an asset and records its identifiers. Matching a physical card to those identifiers is a separate check.
  • That the certificate is still current. A re-certification creates a new certificate for the same asset and marks the earlier one expired. The old certificate's signature still verifies, because it is still the record that was issued.

See it on a certificate

A real certificate shows its signature status at the top, with the Verify offline dialog beside it. The sample certificate has nothing to verify: it is labelled Sample · not signed, because its data is illustrative and no key ever signed it.

Notation

SymbolMeaning
ccThe certificate, without its signed_envelope
JCS(c)\mathrm{JCS}(c)Its RFC 8785 canonical bytes (the payload)
hhSHA-256 digest of the payload
kkThe KMS private key (never leaves KMS)
σ=(r,s)\sigma = (r, s)ECDSA P-256 signature; stored as 64-byte r∥sr \Vert s
id\mathit{id}, asset\mathit{asset}, rom\mathit{rom}, ttreport_id, asset_id, VBIOS ROM SHA-256, generated_at (fingerprint inputs)