Docs menu

Sign Records from Your App (draft)

OpenX has no hosted signing screen. Your app shows the record, sends the person to your own login system, and posts the signature. OpenX checks what arrives, seals it and keeps it, or refuses it and says why.

Goal: Sign a Provenance record from your app, with a login made for that record and meaning.

NOTE

Draft

Every route and field on this page is served. "Wave 3" is the rollout step where OpenX refuses a signature that carries no proof; until then such a signature still lands, as the Provenance guide describes. This page stays a draft until wave 3.

How it works

  1. Your app shows the person the record and the meaning, and says "You are signing this as Approval".
  2. Your app asks OpenX whether this person may sign now.
  3. Your app sends the person to your login system with a forced login. The login request carries a nonce made from the record and the meaning, or a challenge from OpenX.
  4. Your login system asks for the password again and returns an ID token that echoes the nonce.
  5. Your app posts the signature with that ID token as the proof.
  6. OpenX checks the token is from your login system, fresh, the signer's own, made for this record and meaning, and unused. Then it seals and writes the signature.

Your app never makes the proof itself. The proof is the ID token your login system issued.

Before you start

  • Provenance is set up in Environment -> Provenance.
  • Your login system is set up in App Identity. It must give a name claim in its ID tokens.
  • An API Manager rule opens the sign route to your login system.
  • Each signer holds Workspace.Provenance.Sign.
  • Your app calls OpenX as the person, with their own token, as in Call the API from your app.

The "you are signing this" moment

Before the login, your app shows:

  • the record, or the list of records in a batch;
  • the meaning, chosen before the login starts;
  • the sentence "You are signing this as <meaning>".

The person then starts the login. Do not start a login with no meaning chosen.

@o-industrial/common has a kit for this moment (built). On the ./signing key, buildSigningLogin takes the check's OK answer (below), your workspace, the record shown (Record, with its TargetRecordID and BindingDigest) or the list shown (Records), your login system's authorization_endpoint from its discovery document, and your app's return address. It answers the address to send the person to, with the record's nonce, prompt=login and max_age set from the answer's freshness window, and the salt it made. Keep that salt: you post it as Proof.Salt. A list gets the batch nonce instead. The same key carries the nonce functions, the check's shapes and the refusal sentences, and it is safe to import in a browser. On the ./signing/moment key, the SigningMoment component shows the record or the list, the meaning and the sentence, and hands your app the login when the person presses start. With no meaning chosen, or before the check has answered for that meaning, its start is disabled and no login starts.

Ask first: may this person sign now?

POST /ox-api/provenance/signatures/check takes a meaning and a target type. It needs no record and no digest. It judges the person, their right, the meaning and which proof form applies. It writes nothing and spends nothing, so ask as often as you need.

An OK answer carries the login parameters for your login system: the authority, the client, prompt=login and the freshness window. A refusal carries its code and how to fix it (see Refusals).

Ask before your app writes or changes the record. If you ask after, a refusal leaves a record changed and unsigned.

Pick a proof form

FormUse it whenThe login's nonce is
NonceYour app knows the record and its digest before the loginWorked out by your app from the record and the meaning
ChallengeYour app writes the record after the login, so it cannot know the record firstA challenge OpenX gives you, bound to the signer, meaning and target
BatchThe person signs several records at once, from a list they were shownWorked out from the whole list, or a challenge bound to the list

The sign route takes the Nonce and Challenge forms; the batch route takes the Nonce form over the list, or a challenge bound to the list.

Nonce form

  1. Read the record and work out its digest with computeProvenanceBindingDigest from @o-industrial/common (built). Show the record.
  2. Make a random salt of at least 128 bits. Use a new salt for each signing.
  3. Work out the nonce with computeSigningLoginNonce from @o-industrial/common. It takes the workspace, the target type, the record id, the digest, the meaning and the salt.
  4. Send the person to your login system with prompt=login and that nonce.
  5. Post the signature with the ID token and the salt as the proof (below).

OpenX works out the nonce again from its own digest of the record and the meaning, target and salt you posted. The token's nonce must match. So a login made for record A cannot sign record B, and a login made for Review cannot sign as Approval.

POST /ox-api/provenance/signatures
{
  "Pointer": { "ScopeKey": "study-42", "Target": "<target type>", "RecordID": "000000000017" },
  "DisplayedDigest": "<the digest you showed>",
  "SignatureMeaning": "Approval",
  "Proof": { "IdToken": "<the ID token from your login system>", "Salt": "<your salt>" }
}

Add CeremonyID only to join a ceremony that already exists. An optional Comment your app collects from the signer (up to 1000 characters) is kept on the signature.

Challenge form

Use this where your app writes the record as part of the act, such as an Approve button that changes and then signs.

  1. Ask OpenX for a signing challenge with POST /ox-api/provenance/signatures/step-up and { "Signing": { "Meaning": "Approval", "TargetType": "<target type>", "TargetRecordID": "<id, if you know it>" } }. The answer carries the ChallengeID, when it runs out (ExpiresAt) and the Login to run. The request and answer are in the REST reference.
  2. Send the person to your login system with prompt=login and the ChallengeID as the nonce.
  3. Do the write.
  4. Post the signature with "Proof": { "IdToken": "...", "ChallengeID": "..." }.

The challenge is bound to the signer, the meaning, the target type and the record when you named it. It is spent by exactly one signature, and only when that signature is written: if the write fails, the challenge still signs. It runs out after 15 minutes (CHALLENGE_EXPIRED); used again it is CHALLENGE_SPENT; used for another meaning, type or record it is LOGIN_NOT_FOR_THIS_RECORD. It never lets one login sign more than once.

Batch

  1. Show the person the list: each record and the meaning for it.
  2. Work out the batch nonce with computeSigningBatchNonce from @o-industrial/common (built) over the items and a salt, or ask for a challenge bound to the item list (step-up with { "Signing": { "Items" } }). Items are sorted by target type and record id inside the function, so the order you list them in does not matter. buildSigningLogin does this for you when you pass it Records.
  3. One forced login with that nonce.
  4. Post POST /ox-api/provenance/signatures/batch with the items and one proof. Each item is Pointer, DisplayedDigest and Meaning: 1 to 256 records, all in one scope, no record and meaning twice.

The batch is all or nothing. A refusal about one item names it by its Index: a meaning the workspace does not list (400), a record that is not there (404), or a record whose digest does not match (409). Nothing is written. A proof made for records A and B cannot sign A, B and C. The login must be inside the shortest freshness window of the meanings in the list. One login is spent. Each signature opens its own ceremony, so to join a ceremony that already exists, sign that record on its own. Each signature carries ProofForm Batch and the batch's BatchID and BatchManifestDigest.

A 201 answers the signatures in the order you sent them. A 202 means every signature is sealed and accepted and the append is still finishing: do not send the list again. A 503 PROVENANCE_BATCH_PENDING or PROVENANCE_BATCH_JOURNAL_UNAVAILABLE wrote nothing and spent no login: send the list again shortly. The full request and answer are in the REST reference.

Refusals

Every refusal carries a code and a sentence that says how to fix it. Nothing is written when a signature is refused. A refused signing is recorded in the workspace's AccessControl history. The sentences OpenX sends come from the shared refusal table in @o-industrial/common (on the ./signing key); the table below says the same in short.

CodeWhat happenedWhat to do
PROOF_REQUIREDThe signature had no proof (refused from wave 3)Send the person through a forced login and post the ID token
LOGIN_NOT_FOR_THIS_RECORDThe login was made for another record, meaning or listStart a new login for this record and meaning
LOGIN_ALREADY_USEDThis login already signedStart a new login
LOGIN_TOO_OLDThe login is older than the freshness windowAsk the person to log in again
LOGIN_NOT_THE_SIGNERThe login is someone else'sThe signer logs in as themselves
PROOF_NOT_FROM_LOGIN_SYSTEMThe token is not from a login system this workspace trustsUse the login system set up in App Identity
PROOF_HAS_NO_NAMEThe ID token has no name claimTurn on the name claim in your login system
PROOF_MALFORMEDThe proof is not an ID token with either a salt or a challenge, or the salt is under 16 bytesSend { IdToken, Salt } or { IdToken, ChallengeID }
SIGNER_NOT_A_PERSONThe caller is not a person (the check answers it today; the sign route refuses it from wave 3). The reason names which: CONNECTED_APP, APP_SIGNER, DELEGATED_AI or SWAPPED_TOKENSign as the person, with their own login
SIGNER_LOGIN_NOT_TRUSTEDThe person did not log in through the workspace's signing App Identity, for example with an OpenX (Team) sign-in (the check answers it today; the sign route refuses it from wave 3)Sign in through your app
CHALLENGE_EXPIREDThe challenge ran out, or is not knownAsk for a new challenge
CHALLENGE_SPENTThe challenge already signedAsk for a new challenge
SIGNATURE_MEANING_NOT_CONFIGUREDThe meaning is not one this workspace listsPick a meaning from GET /ox-api/provenance/signature-meanings
TWO_PERSON_SAME_SIGNERWith the two-person rule on, the same person signed twiceA different person signs
TWO_PERSON_DIGEST_MISMATCHThe signature joins a ceremony for another recordJoin the ceremony for this record
ACCESS_RIGHT_REQUIREDThe signer does not hold Workspace.Provenance.SignGrant the right in an access configuration
SIGNATURE_SEAL_UNAVAILABLEThe workspace's Key Vault could not seal it. The login is still unusedTry again

A digest that does not match the record answers 409 SIGNATURE_DIGEST_MISMATCH: the record changed after you showed it. Show it again and sign again.

Do not renew the person's token on a signing refusal. A silent renewal could move the login time and turn a refusal into a signature the person did not make. Show the refusal and let the person log in again.

What a signature records

Every signature records SignedBy and SignedAt from the server, Meaning, the record (TargetType, TargetRecordID), BindingDigest, CeremonyID, the seal (LinkSeal, LinkSealKeyID) and the optional Comment.

A signature made with a proof also records SignerPrintedName, from the login's name claim; SignerTitle when your login system gives a job title; SignerIssuer and SignerImmutableID; CallingClient, the app that called; ProofForm (Nonce, Challenge or Batch); the login time and methods (StepUpAt, StepUpMethods); and BatchID with BatchManifestDigest for a batch.

These come from the login and from OpenX, never from your request. A name, title or issuer in the request is ignored. A signature written before these fields existed reads with them absent, never filled in.

Signature events

Subscribe an HTTPS address in Environment -> Provenance -> Signature events. You choose which events to receive:

EventSent when
SignedA signature is written
RefusedA signing is refused
LinkChangedA signature's record is first found changed
  • Delivery is at least once. A failed delivery is tried again up to four times in all, 5 seconds, 30 seconds and 2 minutes apart; after the last it is kept as a dead letter you can read. The newest 100 deliveries per workspace are kept for 30 days.
  • Every body carries an event id, the same on every retry. Keep the ids you have handled, so a second delivery is safe to ignore.
  • Every delivery carries four headers: x-openx-event-id, x-openx-event-type, x-openx-timestamp and x-openx-signature. The signature is v1= followed by the hex HMAC-SHA256, made with the subscription's secret, over the text <timestamp>.<raw body>: the timestamp header's value, a full stop, then the body exactly as received.
  • Work out the HMAC yourself from the raw body, compare it in constant time, and refuse a timestamp more than 300 seconds from your clock.
  • Only a person can set up a subscription. An agent's proposal that carries one is refused (PERSON_ONLY_FIELD).
  • Your app can read the newest deliveries, with each one's state and attempts, from GET /ox-api/provenance/signature-events (it needs Workspace.Provenance.Read). A delivery is named by its subscription, never by its address or secret.

The certificate

GET /ox-api/provenance/signatures/certificate?scopeKey=<scope>&ceremonyId=<id>, or with targetType=<type>&targetRecordId=<id> in place of ceremonyId, answers a printable page. Add format=json for JSON. For each signer it shows the printed name, title, meaning, time in UTC with its zone, the record, BindingDigest, the link status, the seal key and verdict, the proof form, the login time, the login system, the calling app and, for a batch, its list.

Everything is text: no image of a signature. Your app may show the page or link to it.

Verify without us

Anyone can check signatures with no network and no Fathym account. The /verify command in @o-industrial/common takes a signatures export, the public seal keys (GET /ox-api/provenance/signatures/seal-keys) and the exports of the records signed, and prints a verdict per signature: the chain, each seal, and each link where the record is in the export. A changed byte reads Changed; a seal checked against the wrong key reads Invalid; an export with no signatures says so. The command, the files and every verdict are in Verify without us.

Testing

A signature is permanent. In a workspace whose Provenance is Locked, a test signature is kept for the full keep period, and no one can delete it. Even a Protected workspace keeps its records while their keep period runs.

A test path for builders is being worked out. Until then, test in a workspace whose records you are content to keep, and never in the workspace that holds your real records.

The nonce test vector

Use this to check that your app works out the same nonce as OpenX.

InputValue
Workspacews-acme-lab
TargetTypeSample
TargetRecordID000000000042
BindingDigeste3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
MeaningApproval
SaltAAECAwQFBgcICQoLDA0ODw (the 16 bytes 0x00 to 0x0f, base64url)
NonceMZ6tvsi3Ibjajx5itPvmaQhlWbn4Rhl-S1bUeVR6IeE

The nonce is the base64url SHA-256 of a canonical JSON of these fields with a version field. Change any one field, even one character, and the nonce changes. The order of keys in your input does not.

The canonical text that the vector above hashes is exactly this one line, keys in alphabetical order, no spaces, with V the version:

{"BindingDigest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","Meaning":"Approval","Salt":"AAECAwQFBgcICQoLDA0ODw","TargetRecordID":"000000000042","TargetType":"Sample","V":1,"Workspace":"ws-acme-lab"}

The batch vector

A batch nonce hashes the workspace, one salt and the listed items sorted by type, then id, then meaning, then digest. Workspace ws-acme-lab, the same salt as above and these three items give the batch nonce below, whatever order you list them in.

ItemTargetTypeTargetRecordIDBindingDigestMeaning
1Amendment00000000000764 times 1Approval
2Sample000000000042e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Approval
3Sample00000000004364 times 2Review

Batch nonce: bRa4oXrSIEgAOog_J9smUwM73sErulUlPHo7PAmUN_E

"64 times 1" is the 64-character text of sixty-four 1 characters, and the same for 2. The batch text that is hashed, items already sorted:

{"Items":[{"BindingDigest":"1111111111111111111111111111111111111111111111111111111111111111","Meaning":"Approval","TargetRecordID":"000000000007","TargetType":"Amendment"},{"BindingDigest":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","Meaning":"Approval","TargetRecordID":"000000000042","TargetType":"Sample"},{"BindingDigest":"2222222222222222222222222222222222222222222222222222222222222222","Meaning":"Review","TargetRecordID":"000000000043","TargetType":"Sample"}],"Salt":"AAECAwQFBgcICQoLDA0ODw","V":1,"Workspace":"ws-acme-lab"}

The salt is base64url text of at least 16 random bytes in a real signing; the vector's salt is a fixed value only so that you can check your code.

How the workspace chooses the proof

Each workspace sets how signers prove who they are, in Environment -> Provenance -> Signing: the App Identity whose login stamps signatures, the freshness window, and per-meaning rules. A meaning can ask for more than the workspace (a shorter window, or two people), never less.

  • A login from your login system, tied to the record: the default, and the forms on this page.
  • A personal signing key: coming, after the default. The person signs with a key only they hold.

Every signature records which proof it carried.

A person signed in with OpenX's own login (a Team session) does not sign: their login is not the workspace's signing App Identity, so the check answers SIGNER_LOGIN_NOT_TRUSTED, and from wave 3 the sign route does too. Whether a workspace may keep a signature with no tied login after wave 3 is still being decided; this guide will say when it is.

Next steps

On this page