POST /api/session/certificate/challenge
Certificate login, leg 1: present a certificate and receive the 89 bytes to sign.
Begins an X.509 certificate login (AM08). Send the caller's certificate, DER-encoded and
then base64-encoded; receive a pending id and toBeSigned, the 89-byte channel-bound
message to sign.
This route holds an open, half-authenticated key-server connection between the two
legs, which is why the number of concurrent exchanges is bounded and why an unanswered
exchange expires in 30 seconds. The pending id is SINGLE USE: it is consumed by the
first answer, whether that answer succeeds or fails.
The Gateway never sees the private key. The key server resolves the principal by
certificate digest, and only the signature over toBeSigned proves possession -- so
there is no Gateway identity in this flow, and no 503 ..._NOT_CONFIGURED arm.
The construction (ADR-0107). toBeSigned is
"TE-AM08-CHANNEL-BOUND-v1" || 0x00 || SHA-256("tls-exporter:" || exporter) || challenge,
89 bytes, where exporter is the RFC 9266 channel binding of THE GATEWAY'S TLS
connection to the key server and challenge the key server's 32 random bytes (the last
32 bytes of the value). The Gateway is the key server's TLS peer and the relay by design;
the identity the key server establishes records binding=tls-exporter for the Gateway's
channel. The value carries a digest of the exporter, never the exporter, so the caller
learns nothing that satisfies a binding check elsewhere.
Sign ALL 89 bytes under the scheme for your key type; nothing is negotiated.
RSA: crypto.sign("sha256", toBeSigned, { key, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 32 })
-- RSASSA-PSS, SHA-256, MGF1-SHA-256, salt EXACTLY 32 (the key server sets the salt
length exactly; a salt of 20 is refused). EC: crypto.sign("sha256", toBeSigned, { key, dsaEncoding: "der" })
-- ECDSA over SHA-256, DER. A PKCS#1 v1.5 signature over toBeSigned, a signature over
only the challenge, and the raw privateEncrypt primitive this route relayed before
ADR-0107 are each answered SIGNATURE_REJECTED.
WARNING: a conforming signer checks, BEFORE signing, that the value is 89 bytes and begins with the label and a zero byte, and signs nothing else with this key. The label is what makes an AM08 signature unusable as any other signature the key could make -- an EC login key signs ECDSA-SHA256, which is also what an ES256 DPoP proof is -- so a signer that signs whatever it is handed is a signing oracle over bytes chosen upstream of it.
Authentication
No session required. This operation is reachable without an Authorization header. See Authenticating.
Request
| Field | Type | Required | Description |
|---|---|---|---|
certificate | string | yes | The caller's X.509 certificate, DER-encoded then base64-encoded. NOT PEM -- base64 of PEM is base64 of base64, and is refused with a code saying so. |
Example
{
"certificate": "..."
}
Responses
200
A challenge was issued and a key-server connection is being held for you.
| Field | Type | Required | Description |
|---|---|---|---|
pending | string | yes | Single use. Present it to POST /api/session/certificate. |
toBeSigned | string | yes | 89 bytes, base64: the channel-bound message (label, zero byte, digest of the Gateway's channel binding, the key server's 32-byte challenge). Sign ALL of these bytes under the scheme for your key type -- not a hash of them, and not only the challenge. |
expiresInSeconds | number | yes | |
detail | string | yes | Names the scheme |
Example
{
"pending": "...",
"toBeSigned": "...",
"expiresInSeconds": 0,
"detail": "..."
}
400
The certificate could not be used. MISSING_CERTIFICATE, MALFORMED_CERTIFICATE, or CERTIFICATE_NOT_DER -- the last covering both a PEM block and bytes that do not begin with an ASN.1 SEQUENCE tag.
401
SIGNATURE_REJECTED or CERTIFICATE_NOT_ENROLLED, relayed from the key server. Both can arrive on THIS leg rather than the second: the key server refuses an unenrolled or disabled certificate as soon as it sees it, before issuing any challenge. Or CERTIFICATE_REVOKED -- the key server holds revocation evidence naming this certificate (ADR 0115). It is decided with the enrolment check, before any challenge, so it arrives on THIS leg; detail is the key server's sentence naming the issuer and serial. Presenting the same certificate again will not help.
403
PRINCIPAL_DISABLED -- the certificate's principal is disabled. Or REVOCATION_STATUS_UNAVAILABLE -- the key server holds no revocation evidence for this certificate's issuer and that issuer's revocation policy refuses to go without it -- or REVOCATION_EVIDENCE_STALE -- the evidence it holds is older than that policy allows (ADR 0115). Both are decided before any challenge, so both arrive on THIS leg. The certificate itself is not what was refused: an operator must import a current CRL for its issuer. detail is the key server's sentence naming the issuer and the policy.
500
CERT_EXCHANGE_UNEXPECTED -- the key server completed the authentication without issuing a challenge, or the message this Gateway was asked to relay is not shaped like an AM08 channel-bound message. No session was created.
503
CERT_EXCHANGE_LIMIT -- every concurrent exchange slot is in use. Each holds an open key-server connection, so the number is bounded on purpose. Nothing was sent to the key server. Carries Retry-After.
Error handling
Every failure answers JSON carrying at least code and detail. Match on code — detail is written for a human debugging the call and its wording is not part of the contract. See the error model.
| Status | Meaning |
|---|---|
400 | The certificate could not be used. MISSING_CERTIFICATE, MALFORMED_CERTIFICATE, or CERTIFICATE_NOT_DER -- the last covering both a PEM block and bytes that do not begin with an ASN.1 SEQUENCE tag. |
401 | SIGNATURE_REJECTED or CERTIFICATE_NOT_ENROLLED, relayed from the key server. Both can arrive on THIS leg rather than the second: the key server refuses an unenrolled or disabled certificate as soon as it sees it, before issuing any challenge. Or CERTIFICATE_REVOKED -- the key server holds revocation evidence naming this certificate (ADR 0115). It is decided with the enrolment check, before any challenge, so it arrives on THIS leg; detail is the key server's sentence naming the issuer and serial. Presenting the same certificate again will not help. |
403 | PRINCIPAL_DISABLED -- the certificate's principal is disabled. Or REVOCATION_STATUS_UNAVAILABLE -- the key server holds no revocation evidence for this certificate's issuer and that issuer's revocation policy refuses to go without it -- or REVOCATION_EVIDENCE_STALE -- the evidence it holds is older than that policy allows (ADR 0115). Both are decided before any challenge, so both arrive on THIS leg. The certificate itself is not what was refused: an operator must import a current CRL for its issuer. detail is the key server's sentence naming the issuer and the policy. |
500 | CERT_EXCHANGE_UNEXPECTED -- the key server completed the authentication without issuing a challenge, or the message this Gateway was asked to relay is not shaped like an AM08 channel-bound message. No session was created. |
503 | CERT_EXCHANGE_LIMIT -- every concurrent exchange slot is in use. Each holds an open key-server connection, so the number is bounded on purpose. Nothing was sent to the key server. Carries Retry-After. |