Skip to content

@shutter-network/concorde/signatures

The HTTP routes this component serves: 1 on the Agent server and 2 on the Public server.

Every path is relative. Each one is printed as the plugin declares it, under whatever prefix the component's constructor registers that plugin at. This page states no mount point, because the constructor is where the mount point is decided.

Generated from what the route plugins in src/signatures/routes.ts declare, which is what a running Gateway serves at GET /openapi.json. Never edited by hand.

Agent server

The Agent server has no authentication of any kind. Everything below is reachable by the agent, and therefore by an injected prompt (ADR-0003). Nothing on it names a credential.

POST /sign

Sign a Statement

Have any string signed with the shared agent's key, and receive one compact JWS back. Nothing is stored. There is no row afterwards and no route that lists what has been signed, so the artifact answered is the whole of what happened. Losing it means signing again. A commitment worth keeping is a Decision: POST /decisions signs it too, and numbers it, and keeps it.

The typ is yours, and nothing is reserved. Asking for concorde-decision+jws here is allowed and is not a forgery. Publishing a Decision is an authority you already hold, so a decision-typed artifact minted here is that same authority exercised without a log row. What it is not is a promise to a verifier. typ is your signed claim about your own artifact, and only an artifact fetched from GET /decisions is guaranteed to be shaped like one.

The Statement has no length limit of ours. The server's own body limit is the bound, and it belongs to the Operator. An unknown query parameter is a 400, not a filter that did nothing and a request answered with everything.

Tagged Signatures.

Request body

application/json, required.

  • statement string, required, at least 1 character

  • typ string, optional, 1 to 128 characters, default "concorde-statement+jws"

    What kind of thing this artifact is. It goes into the protected header, so the signature covers it. Swapping it invalidates the artifact, which keeps a receipt from being presented as an approval.

    Any label, concorde-decision+jws included. Domain separation between your own categories, such as receipts, votes and approvals, is something only you can express. Give each of them a label of its own, or they collapse into one domain and become replayable as each other. Defaults to concorde-statement+jws.

Responses

200 application/json

The Signed Statement, which is the whole answer: this part kept no copy of it and no record that it was made, beyond one line in the log carrying the typ and a digest.

  • jws string, required

    The Signed Statement: a compact JWS, header.payload.signature, base64url, one URL-safe string. Its payload is {"statement":…} and nothing else, with no number and no timestamp, both of those being a Decision's. Its protected header carries the algorithm and the typ that was asked for. Nothing was stored: this string is the whole of what happened, so keep it or hand it on.

400 application/json

statement is missing or empty, typ is empty or longer than 128 characters, or a query parameter was written.

  • statusCode integer, required
  • error string, required
  • message string, required

Public server

The Public server is what a User's client calls. Which credential each route wants, and which wants none, is in that route's own description below.

GET /jwks.json

Fetch the shared agent's public key

The shared agent's public key, as a JWK Set (RFC 7517). It is what makes a Signed Statement checkable without trusting this Gateway. Fetch it once, keep it, and verify artifacts offline with any JOSE library in any language. That is the real verification path, and the only one worth anything to somebody who does not trust the Operator.

One keypair, always, so the set holds one key. There is no rotation, no key identifier and nothing to select between. What a valid signature proves is narrow. The Operator committed to this Statement on the shared agent's behalf. It says nothing whatever about how the agent behaved.

No Token is required, a public key being public. Besides the login itself, this is the one route on the Public server that asks for none. An unknown query parameter is a 400, not a filter that did nothing and a request answered with everything.

Tagged Signatures.

Responses

200 application/json

The key set, holding the one key this shared agent signs with. It carries no private member: the schema is a positive list of public ones, so there is nothing here to keep secret and nothing to send it over TLS for.

  • keys array of object, required
    • kty string, required

      The key type, in JOSE's own vocabulary rather than OpenSSL's: OKP for an Edwards key, EC for a NIST curve, RSA for an RSA key.

    • crv string, optional

    • x string, optional

    • y string, optional

    • n string, optional

    • e string, optional

400 application/json

A query parameter was written, and this route takes none.

  • statusCode integer, required
  • error string, required
  • message string, required

POST /verify

Check whether an artifact is this shared agent's

A convenience, and it proves less than it looks like it proves. It answers one question: is this artifact this shared agent's? You have to believe the answer, because a dishonest Gateway says true to anything. So it is worth nothing to the party this identity exists for. That party does not trust the Operator. It is genuinely useful to a User, who trusts the Operator already. They want a quick confirmation without embedding a JOSE library.

Real verification is offline. Fetch GET /jwks.json once, keep the key, and check the artifact yourself in whatever language you are already writing. That asks this Gateway nothing. The path is open to anybody holding the string, needs no Token, and keeps working after this deployment is gone.

Answers 200 either way. An artifact that is not ours is a false and not an error. That covers a foreign identity's artifact, a tampered one and a wrong number of segments. Malformed base64url and an unparseable header get the same answer. All of those arrive from a caller, and none of them is this Gateway failing. Requires a bearer Token, presented as Authorization: Bearer <token> and obtained from POST /auth/tokens. The User acted on is the one the Gateway authenticated, and no parameter anywhere names another. An unknown query parameter is a 400, not a filter that did nothing and a request answered with everything.

Tagged Signatures.

Request body

application/json, required.

  • jws string, required, at least 1 character

Responses

200 application/json

The verdict, and the artifact's own header and payload when it is ours. Both of those are read out of the bytes the signature covers, so they are what was signed rather than what was posted.

  • verified boolean, required

    Whether this artifact carries a valid signature by this shared agent's key. false covers every way of not being ours at once: another identity's artifact, a tampered one, a wrong number of segments, malformed base64url and an unparseable header alike. None of them is an error, so none of them is a 4xx.

  • header any JSON, optional

    The protected header the signature covers, as it was written: the algorithm, and the typ the signer chose. typ is that signer's own claim about its artifact and not a guarantee of this framework's. A "concorde-decision+jws" here means this identity labelled it a Decision, not that it is shaped like one and not that a row exists. Only an artifact fetched from GET /decisions is guaranteed well-formed.

  • payload any JSON, optional

    The claims the signature covers, parsed out of the bytes that were signed. A Statement signed at POST /sign carries { statement }; a Decision carries { seq, createdAt, statement }, so everything a Decision record holds can be read back out of the artifact alone.

400 application/json

jws is missing or empty, or a query parameter was written.

  • statusCode integer, required
  • error string, required
  • message string, required

401 application/json

Authentication failed, which is the whole of what is said: a wrong password, an id nobody holds, a User with no password, and a Token that is missing, malformed, unknown or expired are one status and one message, so nothing here answers who exists. This part authenticates nobody: the refusal is publicServer.requireUser, taken as one option on the route, so it is the same 401 every protected route on this server answers, whichever scheme the deployment accepts.

  • statusCode integer, required
  • error string, required
  • message string, required