> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qanapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify the hash chain

> Requires `audit:read`. Recomputes the chain over a range without
exporting it, covering at most 100,000 records. Each record is an HMAC
over the whole record and its predecessor's hash, so editing any field
of any record breaks every link after it.

`gaps` are distinguished from `broken_links` deliberately. A filtered
query legitimately produces gaps, and so does an authorised purge; a
broken link is the finding that matters. Purge windows are consulted, so
a gap explained by a recorded purge is listed under
`explained_by_purge` rather than reported as tampering.

Chains are maintained per node, so verifying one `instance_id` at a time
is the normal way to use this.

This endpoint accepts every filter the query endpoint accepts. The ones
below are the ones worth using; `page` and `per_page` are accepted and
ignored.




## OpenAPI

````yaml /openapi/stand-alone/v3.yaml get /api/v3/audit-logs/verify
openapi: 3.1.0
info:
  title: Qanapi Standalone API
  version: 3.0.0
  summary: Single-tenant encryption, key management, classification and audit service.
  description: >
    A standalone cryptographic service. Callers hand it data and it returns

    ciphertext, or hand it ciphertext and it returns data, with every operation

    authorised by policy and recorded on a tamper-evident audit trail.


    ## Authentication


    Two credentials reach the same principal:


    - **API key** in `X-Qanapi-Authorization`, for machine callers. A key
    reaches
      only the configurations it is linked to.
    - **User bearer token** in `Authorization`, from `POST /api/v3/auth/login`,
      for a person working through a console.

    An API key outranks a bearer token when a request carries both, because a

    key names a narrower credential than the user that owns it.


    ## Request bodies


    Endpoints that take a JSON body require `Content-Type: application/json`.

    The two encryption endpoints are the exception: in whole-body mode they

    accept any bytes at all, whatever the content type says.


    ## Errors


    Every error body carries `message`, which is human readable, and `error`, a

    stable machine readable code that clients can branch on. Validation failures

    additionally carry an `errors` object keyed by the field each message

    concerns; a failure with no single field to blame is filed under `request`.


    ## Rate limiting


    Every response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`. The

    window is per node and keyed on the client address, so a cluster of n nodes

    admits n times the configured figure.


    `POST /api/v3/auth/login` is limited separately and more strictly, ten a

    minute against sixty, because it needs no credential to reach and each

    attempt costs a full password verification. A successful login clears that

    budget, so the stricter limit falls on failed attempts rather than on

    legitimate sign-ins.


    ## Correlation


    Send `X-Request-Id` to tie a request to your own trace. The value is

    preserved on the audit record and echoed on the response, provided it is at

    most 64 characters of letters, digits, `-`, `_`, `.` or `:`; anything else

    is replaced with a generated id. Omit the header and one is generated for

    you.


    ## End-to-end encrypted requests and responses


    Any endpoint accepts a request body wrapped as an RFC 7516 JWE, so the

    payload is unreadable by anything between your client and this service,

    including the reverse proxy that terminates TLS. Wrap the JSON body to the

    key from `GET /api/v3/qanapi-public-key` and send it with

    `Content-Type: application/jose`; `application/jwt` and

    `application/jose+json` are accepted too, and a body that is plainly a

    compact JWE is recognised even without one of those content types. Where the

    installation requires it, the wrapped payload must carry `iat`, `exp` and

    `jti` claims, which also make each payload single-use.


    To have the **response** wrapped in turn, send

    `X-Qanapi-Wrap-With-Key-Id` naming an RSA key held in this service's KMS

    that you hold the private half of. The response then arrives as a compact

    JWE with `Content-Type: application/jose`. The header spelling

    `wrap_with_key_id` is accepted as well. The wrapping key is resolved with

    the same permission a plain key read needs, so a caller who may not read

    that key gets an error instead of a wrapped body.
  license:
    name: Proprietary
    identifier: LicenseRef-Qanapi-Commercial
servers:
  - url: https://{host}
    description: A deployed instance, behind the reverse proxy that terminates TLS.
    variables:
      host:
        default: qanapi.example.com
  - url: http://localhost:8000
    description: A local cluster, through its load balancer.
security:
  - ApiKeyAuth: []
  - BearerAuth: []
tags:
  - name: STAND Health
    description: >
      Liveness and readiness probes. Registered outside the authentication,
      audit

      and transport layers, so a load balancer polling them without credentials

      still gets an answer.
  - name: STAND Authentication
    description: >
      Logging in. There is no registration endpoint: the first administrator is

      created from the command line on the host, so nothing reachable over the

      network can claim an installation that has no users yet. Later accounts
      are

      created through `POST /api/v3/users`.
  - name: STAND Users
    description: User accounts and their roles.
  - name: STAND API keys
    description: >
      Machine credentials. A key is scoped to a set of configurations and
      reaches

      nothing until it is linked to at least one.
  - name: STAND Configurations
    description: >
      Encryption configurations. A configuration owns a master key and is the
      unit

      of cryptographic isolation.
  - name: STAND Classifications
    description: Sensitivity labels and the clearance that reaches them.
  - name: STAND Policies
    description: Allow and deny rules binding a principal to actions on resources.
  - name: STAND Encryption
    description: >
      The data plane. Field-level or whole-body encryption under a
      configuration,

      optionally forwarded onward to a destination of the caller's choosing.
  - name: STAND KMS
    description: Customer-managed key lifecycle, import and export.
  - name: STAND Audit
    description: The tamper-evident record of everything the service did.
  - name: STAND Transport
    description: The service's own key pair, for end-to-end encrypted request bodies.
paths:
  /api/v3/audit-logs/verify:
    get:
      tags:
        - STAND Audit
      summary: Verify the hash chain
      description: |
        Requires `audit:read`. Recomputes the chain over a range without
        exporting it, covering at most 100,000 records. Each record is an HMAC
        over the whole record and its predecessor's hash, so editing any field
        of any record breaks every link after it.

        `gaps` are distinguished from `broken_links` deliberately. A filtered
        query legitimately produces gaps, and so does an authorised purge; a
        broken link is the finding that matters. Purge windows are consulted, so
        a gap explained by a recorded purge is listed under
        `explained_by_purge` rather than reported as tampering.

        Chains are maintained per node, so verifying one `instance_id` at a time
        is the normal way to use this.

        This endpoint accepts every filter the query endpoint accepts. The ones
        below are the ones worth using; `page` and `per_page` are accepted and
        ignored.
      operationId: verifyAuditChain
      parameters:
        - $ref: '#/components/parameters/AuditFrom'
        - $ref: '#/components/parameters/AuditTo'
        - $ref: '#/components/parameters/AuditInstanceId'
        - $ref: '#/components/parameters/AuditAction'
        - $ref: '#/components/parameters/AuditPrincipalId'
        - $ref: '#/components/parameters/AuditConfigurationId'
      responses:
        '200':
          description: The chain verified over the range examined.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChainVerification'
              example:
                verified: true
                records: 4812
                broken_links: []
                gaps:
                  - 27ce8b04-6f1d-4a39-81b7-3d05c92eaf68
                explained_by_purge:
                  - 27ce8b04-6f1d-4a39-81b7-3d05c92eaf68
                gaps_expected: true
        '400':
          $ref: '#/components/responses/NoCredential'
        '401':
          $ref: '#/components/responses/BadCredential'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: |
            The chain did not verify. The body is the same summary, naming what
            failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChainVerification'
components:
  parameters:
    AuditFrom:
      name: from
      in: query
      description: Only records at or after this instant.
      schema:
        type: string
        format: date-time
    AuditTo:
      name: to
      in: query
      description: Only records at or before this instant.
      schema:
        type: string
        format: date-time
    AuditInstanceId:
      name: instance_id
      in: query
      description: |
        Only records written by this cluster node. Hash chains are maintained
        per node, so this is the natural unit for chain verification.
      schema:
        type: string
        examples:
          - node-01
    AuditAction:
      name: action
      in: query
      description: |
        An exact action, or a `kms.*` style prefix that matches a whole
        namespace.
      schema:
        type: string
        examples:
          - encryption.encrypt
          - kms.*
    AuditPrincipalId:
      name: principal_id
      in: query
      description: |
        Only records attributed to this principal: a user id or an API key id.
      schema:
        type: string
    AuditConfigurationId:
      name: configuration_id
      in: query
      description: Only records attributed to this configuration.
      schema:
        type: string
        format: uuid
  schemas:
    ChainVerification:
      type: object
      required:
        - verified
        - records
        - broken_links
        - gaps
        - explained_by_purge
        - gaps_expected
      properties:
        verified:
          type: boolean
          description: |
            False when any link is broken, or when a gap is not accounted for by
            a recorded purge and the filters do not explain it.
        records:
          type: integer
          description: Records examined.
        broken_links:
          type: array
          description: |
            The finding that matters: the id of each record whose hash does not
            follow from its predecessor, which means it was edited after it was
            written.
          items:
            type: string
            format: uuid
        gaps:
          type: array
          description: |
            The id of each record whose predecessor is missing. Expected
            whenever the query is filtered, and expected after an authorised
            purge, so this is reported separately from a broken link rather than
            raised as tampering.
          items:
            type: string
            format: uuid
        explained_by_purge:
          type: array
          description: |
            The subset of `gaps` that a recorded purge accounts for. Those
            records live on in the export that the purge produced.
          items:
            type: string
            format: uuid
        gaps_expected:
          type: boolean
          description: |
            Whether the filters themselves narrow the range, in which case gaps
            carry no information.
    Error:
      type: object
      required:
        - message
        - error
      properties:
        message:
          type: string
          description: |
            Human readable. A server-side fault is described by its category and
            nothing more: operational detail stays in the service's own log.
          examples:
            - Unauthorized.
        error:
          type: string
          description: |
            A stable machine readable code. Branch on this rather than on
            `message`, which is shared between conditions that this code tells
            apart.
          examples:
            - InvalidApiKeyException
  responses:
    NoCredential:
      description: |
        No credential was presented. Send an API key in
        `X-Qanapi-Authorization`, or a bearer token in `Authorization`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: Unauthorized.
            error: Unauthenticated
    BadCredential:
      description: A credential was presented and did not verify.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: Unauthorized.
            error: InvalidApiKeyException
    Forbidden:
      description: |
        Authenticated, but not permitted. Also returned for a revoked API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: Invalid permission to perform action
            error: AccessDenied
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Qanapi-Authorization
      description: |
        A machine credential, `qk_` followed by its secret. Only a hash of the
        secret is stored, so a key is displayed exactly once, when it is created
        or rotated. A key reaches only the configurations it is linked to.

        It may also be sent as `Authorization: Bearer qk_...`, which is
        recognised by the prefix.
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        A user token from `POST /api/v3/auth/login`. The token asserts a role,
        but the database decides it, so a role changed or a user deleted after
        the token was issued takes effect immediately. A token minted before a
        password change is refused.

````