POST /api/v1/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. 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 a raw privateEncrypt of the challenge 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/v1/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, in Node terms. |
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_RETIRED -- the key server holds this certificate as EXPIRED: it was renewed or replaced, and only its replacement authenticates. The principal, its login id and so the service registration are unchanged. Or CERTIFICATE_REVOKED -- the key server holds revocation evidence naming this certificate. 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. 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. Or CAPACITY_UNAVAILABLE (scope: process, Retry-After) -- this Gateway is at its request ceiling (see "Rate and capacity limits").
413
PAYLOAD_TOO_LARGE (retryable: false) -- the request body is larger than this
deployment accepts. It was refused before it was parsed, and the connection is closed.
Nothing was sent to the key server.
429
RATE_LIMITED (retryable: true, Retry-After) -- the allowance this request is charged
to is spent: the login allowance of its network source (before authentication), or the
allowance, the concurrency ceiling or the live-session ceiling of the identity it was
verified as -- shared by every session that identity holds, including every delegated
and scoped credential a service minted. It carries no scope. Nothing was sent to the
key server; on a login refused after authentication, the session it created has been
closed. See "Rate and capacity limits" above.
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_RETIRED -- the key server holds this certificate as EXPIRED: it was renewed or replaced, and only its replacement authenticates. The principal, its login id and so the service registration are unchanged. Or CERTIFICATE_REVOKED -- the key server holds revocation evidence naming this certificate. 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. 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. Or CAPACITY_UNAVAILABLE (scope: process, Retry-After) -- this Gateway is at its request ceiling (see "Rate and capacity limits"). |
413 | PAYLOAD_TOO_LARGE (retryable: false) -- the request body is larger than this deployment accepts. It was refused before it was parsed, and the connection is closed. Nothing was sent to the key server. |
429 | RATE_LIMITED (retryable: true, Retry-After) -- the allowance this request is charged to is spent: the login allowance of its network source (before authentication), or the allowance, the concurrency ceiling or the live-session ceiling of the identity it was verified as -- shared by every session that identity holds, including every delegated and scoped credential a service minted. It carries no scope. Nothing was sent to the key server; on a login refused after authentication, the session it created has been closed. See "Rate and capacity limits" above. |