Skip to content

Instantly share code, notes, and snippets.

@cre8
Last active April 8, 2025 22:40
Show Gist options
  • Save cre8/7719ca4de012ab86f1c13319959c4732 to your computer and use it in GitHub Desktop.
Save cre8/7719ca4de012ab86f1c13319959c4732 to your computer and use it in GitHub Desktop.
Signed Certificate Timestamp for JWS

Signed Certificate Timestamp for JWS

The SCT for JWS is not a requirement of the regulation, but was defined to test it out. It is not mandatory to implement it because there is no official standard for it.

Since the registration certificate is a JWS instead of x509, the reference to the certificate transparency log servers is implemented in another way. The log servers will create a signed certificate timestamp (SCT) that will be attached to the JWS unprotected header of the registration certificate that the registrar is issuing. The signature verification of the registration certificate does not depend on the SCTs in the JWS header. The verification process only involves the protected, payload, and signature fields. The SCTs are only used to generate transparency ensuring that the registrar does not issue certificates that are not publicly available or certificates that could be misused by unauthorized parties.

A registration certificate can have multiple SCTs attached to the header. Each entry includes

  • sth: the signed tree head that is signed by the log server.
  • mip: the merkle inclusion proof that is used to validate the SCT.

The required amount of SCTs passed to the JWS depends on the policy of the registrar.

{
  "header": {
    "scts": [
      {
        "sth": "", //signed tree head
        "mip": {} //merkle inclusion proof
      }
    ]
  }
}

Signed Tree Head (STH)

The signed tree head is represented as a JWT instead of the RFC binary format, allowing the key for signature validation to be referenced. Instead of the log id, the certificate including the public key is added in the x5c field.

TODO: evaluate if the certificate is self signed or issued by a trust party or the CA provider that is using this log server (when the log service is used by multiple registrar certificate providers, the first option is better)

{
  "header": {
    "alg": "ES256",
    "x5c": ["MIIDQjCCA..."]
  },
  "payload": {
    "tree_head": "UrttpelEB8JvrRWS6onFBqrkSZEgmNfFXYxAR6rRdFY",
    "tree_size": 46,
    "tree_hash": "sha256",
    "iat": 1741618017222,
  },
  "signature": "-K7eQ9CXjiELpAROEdgOdInHbMYoA2ZB-gqiFGuO2rzTuwkCXkG1Kr58PqLfgNHNceupNaYt7LxTw2jDd4UeKw"
}
  • tree_head REQUIRED. The base64url encoded root hash of the Merkle tree.
  • tree_size REQUIRED. The size (number of elements) of the tree.
  • tree_hash REQUIRED. The hash algorithm used for the Merkle tree as defined in the IANA Named Information Hash Algorithm Registry.
  • iat REQUIRED. The time of the issuance of the STH. See RFC7519 for more information.

Merkle Inclusion Proof (MIP)

The inclusion proof is required to check if a specific value is included in the Merkle tree. The MIP does not need a signature since the SCT is already signed by the log server and serves as a proof that the values are correct. If the merkle inclusion proof is not valid the SCT is not valid.

The Wallet needs to calculate the leave hash based on the Registration Certificate to validate the MIP. The leave hash is calculated as follows:

  const header = Buffer.from(
    JSON.stringify(cert.jws.protected),
    'utf-8',
  ).toString('base64url');
  const payload = Buffer.from(
    JSON.stringify(cert.jws.payload),
    'utf-8',
  ).toString('base64url');
  const jws = `${header}.${payload}.${cert.jws.signature}`;
  // Prepend the CT leaf prefix (0x00) to the UTF-8 encoded JWS string.
  const prefix = Buffer.from([0x00]);
  const jwsBuffer = Buffer.from(jws, 'utf-8');
  const dataToHash = Buffer.concat([prefix, jwsBuffer]);

  // Compute and return the SHA-256 hash, based on tree_hash.
  return createHash('sha256').update(dataToHash).digest();
{
  "leaf_index": 45,
  "inclusion_path": [
    "SbGRAd6D9mF-6AayqmJ_9LDGR-xEKJazBv56pKN0qxg",
    "EJeSliCdFUfBbG4vkhs1Pat7eNzf_0UDeDh8aYGuCYY",
    "RgqMEfIY8p8cGxb10sjrTPZKpM77EdjGVaddSKsshZQ",
    "8Oztsnyri7QwB2AnC2vmFLG2IFTyfI0s3PG3-wAcf6w"
  ],
  "tree_size": 46,
  "tree_hash": "sha256"
}
  • tree_size REQUIRED. The size (number of elements) of the tree on which this inclusion proof is based.
  • tree_hash REQUIRED. The hash algorithm used for the Merkle tree as defined in the IANA Named Information Hash Algorithm Registry.
  • leaf_index REQUIRED. The 0-based index of the log entry corresponding to this inclusion proof.
  • inclusion_path REQUIRED. A vector of base64url encoded Merkle Tree nodes, proving the inclusion of the chosen JWS, as described in RFC 9162 Section 2.1.3.

Merkle Consistency Proof (MCP)

After validating the SCT and the MIP, the Wallet needs to verify if the Merkle tree is a valid subtree of the current Merkle tree of the log server. The Wallet needs to fetch the consistency proof between the given STH and the latest one:

GET /ct/get-sth-consistency?first=46
Host: log.funke-wallet.de
Content-Type: application/json

The first query parameter is the tree size of the first STH from the SCT and the response will include the latest STH and the consistency proof that the Wallet needs to validate.

HTTP/101 200 OK
Content-Type: application/json
{
  "consistency": [
    "ZcAki5ok68JyMddAPAidxzzEgZ-vr3_5tIzvYM7jS2U",
    "wNsycQ8UKh4mHkrE_WPY7XKie5ZHGbSJxqpdWSUUCQE",
    "EJeSliCdFUfBbG4vkhs1Pat7eNzf_0UDeDh8aYGuCYY",
    "RgqMEfIY8p8cGxb10sjrTPZKpM77EdjGVaddSKsshZQ",
    "Do1C3B7DUIQF3PDyrq-xi6GasPgB6tgzQ4fFQmw-DU0",
    "8Oztsnyri7QwB2AnC2vmFLG2IFTyfI0s3PG3-wAcf6w",
    "xeqXTAJfmRcw6U3dQkXWGScbTirLaURacR6p7E-aUEM",
    "N9Rp8x0DtMJ41-bNuP1481q52FHl8bbcNWm0LlrVJu4",
    "GEy893U8husk22m_P1FwnLbVStE7jIkIXB_XahM-eFg"
  ],
  "sth": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImY5OGFlMzkyLWE0MTYtNGNlMy05NGZiLWFhYTExNmI0MjNiMiJ9.eyJ0cmVlSGVhZCI6IktNa21ndEhGdVlraHg0a1hjaDZvZVVNV2czdzIxeGZoK0IwOGtWUUhaZ2c9IiwidHJlZVNpemUiOjM0NywiaWF0IjoxNzQxNjE5MzA3OTU2LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEifQ.ZZ6U3cA7HsSIsHW8fhb0PxL9iThE-111HGZLyzqS_exN21VxiCS8Xm2lzUYAsfKURL00VU624Vdug0o3hZQCXw"
}
  • consistency REQUIRED. An array of Merkle Tree nodes (base64url encoded) proving the consistency between the two STHs, as described in RFC 9162 Section 2.1.4.
  • sth REQUIRED. The new STH that the Wallet needs to validate.

After the validation of the consistency proof, the Wallet can be sure that the SCT is valid and the registration certificate is public available. To guarantee a freshness, the SCT's timestamp should not be older than defined in the log server's policy.

Security Considerations

Validating the merkle inclusion proof and the merkle consistency proof avoids a split-view attack where the log operator could present different historical trees to different clients. It also proofs to the client that the log is append-only and that the inclusion proof is still valid in the latest state of the log.

To avoid the single point of failure, the Wallet should validate multiple of the provided SCTs that got issued by different log servers. More considerations can be found in the RFC 9162.

The definition of auditors that are checking the logs for consistency and correctness is out of scope for this document for now.

Privacy Considerations

The fetching of the consistency proof has to be done by the Relying Party that will pass these information to the Wallet in the presentation request together with the registration request. By doing so, the Wallet must not interact with the log servers directly, ensuring that the log server provider does not know which Registration Certificate is requested by which Wallet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment