@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.
statementstring, required, at least 1 charactertypstring, 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+jwsincluded. 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 toconcorde-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.
jwsstring, requiredThe 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 thetypthat 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.
statusCodeinteger, requirederrorstring, requiredmessagestring, 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.
keysarray ofobject, requiredktystring, requiredThe key type, in JOSE's own vocabulary rather than OpenSSL's:
OKPfor an Edwards key,ECfor a NIST curve,RSAfor an RSA key.crvstring, optionalxstring, optionalystring, optionalnstring, optionalestring, optional
400 application/json
A query parameter was written, and this route takes none.
statusCodeinteger, requirederrorstring, requiredmessagestring, 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.
jwsstring, 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.
verifiedboolean, requiredWhether this artifact carries a valid signature by this shared agent's key.
falsecovers 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.headerany JSON, optionalThe protected header the signature covers, as it was written: the algorithm, and the
typthe signer chose.typis 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 fromGET /decisionsis guaranteed well-formed.payloadany JSON, optionalThe claims the signature covers, parsed out of the bytes that were signed. A Statement signed at
POST /signcarries{ 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.
statusCodeinteger, requirederrorstring, requiredmessagestring, 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.
statusCodeinteger, requirederrorstring, requiredmessagestring, required