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
- Your app shows the person the record and the meaning, and says "You are signing this as Approval".
- Your app asks OpenX whether this person may sign now.
- 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.
- Your login system asks for the password again and returns an ID token that echoes the nonce.
- Your app posts the signature with that ID token as the proof.
- 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
| Form | Use it when | The login's nonce is |
|---|---|---|
| Nonce | Your app knows the record and its digest before the login | Worked out by your app from the record and the meaning |
| Challenge | Your app writes the record after the login, so it cannot know the record first | A challenge OpenX gives you, bound to the signer, meaning and target |
| Batch | The person signs several records at once, from a list they were shown | Worked 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
- Read the record and work out its digest with
computeProvenanceBindingDigestfrom@o-industrial/common(built). Show the record. - Make a random salt of at least 128 bits. Use a new salt for each signing.
- Work out the nonce with
computeSigningLoginNoncefrom@o-industrial/common. It takes the workspace, the target type, the record id, the digest, the meaning and the salt. - Send the person to your login system with
prompt=loginand that nonce. - 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.
- Ask OpenX for a signing challenge with
POST /ox-api/provenance/signatures/step-upand{ "Signing": { "Meaning": "Approval", "TargetType": "<target type>", "TargetRecordID": "<id, if you know it>" } }. The answer carries theChallengeID, when it runs out (ExpiresAt) and theLoginto run. The request and answer are in the REST reference. - Send the person to your login system with
prompt=loginand theChallengeIDas the nonce. - Do the write.
- 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
- Show the person the list: each record and the meaning for it.
- Work out the batch nonce with
computeSigningBatchNoncefrom@o-industrial/common(built) over the items and a salt, or ask for a challenge bound to the item list (step-upwith{ "Signing": { "Items" } }). Items are sorted by target type and record id inside the function, so the order you list them in does not matter.buildSigningLogindoes this for you when you pass itRecords. - One forced login with that nonce.
- Post
POST /ox-api/provenance/signatures/batchwith the items and one proof. Each item isPointer,DisplayedDigestandMeaning: 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.
| Code | What happened | What to do |
|---|---|---|
PROOF_REQUIRED | The signature had no proof (refused from wave 3) | Send the person through a forced login and post the ID token |
LOGIN_NOT_FOR_THIS_RECORD | The login was made for another record, meaning or list | Start a new login for this record and meaning |
LOGIN_ALREADY_USED | This login already signed | Start a new login |
LOGIN_TOO_OLD | The login is older than the freshness window | Ask the person to log in again |
LOGIN_NOT_THE_SIGNER | The login is someone else's | The signer logs in as themselves |
PROOF_NOT_FROM_LOGIN_SYSTEM | The token is not from a login system this workspace trusts | Use the login system set up in App Identity |
PROOF_HAS_NO_NAME | The ID token has no name claim | Turn on the name claim in your login system |
PROOF_MALFORMED | The proof is not an ID token with either a salt or a challenge, or the salt is under 16 bytes | Send { IdToken, Salt } or { IdToken, ChallengeID } |
SIGNER_NOT_A_PERSON | The 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_TOKEN | Sign as the person, with their own login |
SIGNER_LOGIN_NOT_TRUSTED | The 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_EXPIRED | The challenge ran out, or is not known | Ask for a new challenge |
CHALLENGE_SPENT | The challenge already signed | Ask for a new challenge |
SIGNATURE_MEANING_NOT_CONFIGURED | The meaning is not one this workspace lists | Pick a meaning from GET /ox-api/provenance/signature-meanings |
TWO_PERSON_SAME_SIGNER | With the two-person rule on, the same person signed twice | A different person signs |
TWO_PERSON_DIGEST_MISMATCH | The signature joins a ceremony for another record | Join the ceremony for this record |
ACCESS_RIGHT_REQUIRED | The signer does not hold Workspace.Provenance.Sign | Grant the right in an access configuration |
SIGNATURE_SEAL_UNAVAILABLE | The workspace's Key Vault could not seal it. The login is still unused | Try 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:
| Event | Sent when |
|---|---|
Signed | A signature is written |
Refused | A signing is refused |
LinkChanged | A 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-timestampandx-openx-signature. The signature isv1=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 needsWorkspace.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.
| Input | Value |
|---|---|
| Workspace | ws-acme-lab |
| TargetType | Sample |
| TargetRecordID | 000000000042 |
| BindingDigest | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
| Meaning | Approval |
| Salt | AAECAwQFBgcICQoLDA0ODw (the 16 bytes 0x00 to 0x0f, base64url) |
| Nonce | MZ6tvsi3Ibjajx5itPvmaQhlWbn4Rhl-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.
| Item | TargetType | TargetRecordID | BindingDigest | Meaning |
|---|---|---|---|---|
| 1 | Amendment | 000000000007 | 64 times 1 | Approval |
| 2 | Sample | 000000000042 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 | Approval |
| 3 | Sample | 000000000043 | 64 times 2 | Review |
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
- What OpenX and your app each own, clause by clause -> Validation Pack
- Reading and exporting signatures -> Activity Log
- Setting up the record -> Provenance
On this page
- Sign Records from Your App (draft)
- ╰─▶How it works
- ╰─▶Before you start
- ╰─▶The "you are signing this" moment
- ╰─▶Ask first: may this person sign now?
- ╰─▶Pick a proof form
- ╰─▶Nonce form
- ╰─▶Challenge form
- ╰─▶Batch
- ╰─▶Refusals
- ╰─▶What a signature records
- ╰─▶Signature events
- ╰─▶The certificate
- ╰─▶Verify without us
- ╰─▶Testing
- ╰─▶The nonce test vector
- ╰─▶How the workspace chooses the proof
- ╰─▶Next steps