Public API · Cloud Signature Consortium v2 · Tier C1 subset
/csc/v2 · remote-signature service.
A Cloud Signature Consortium (CSC) API v2 Tier C1 subset on CodeB Sovereign Communications. Each authenticated user gets an EC P-256 signing credential whose Subject Distinguished Name is enriched from the OIDC profile (CN = full name, given / family / e-mail with SAN rfc822Name). Signed hashes come back per RFC 5652 CMS; server-side signDoc assembles the full PAdES-B-B / B-T envelope; an RFC 3161 TSA proxy at signatures/timestamp upgrades a client-assembled signature to PAdES-B-T.
Scope disclosure — Advanced Electronic Signature only.
This remote-signature service produces AdES per Regulation (EU) 910/2014 Article 3(11). The signing key is software-backed (per-tenant PFX; per-user credential in Phase 1b) and the certificate is self-signed by an in-tenant CA — it is not a Qualified Signature Creation Device (QSCD) and not a Qualified Electronic Signature (QES). The ICryptoModule boundary is HSM-ready (Azure Key Vault and PKCS#11 back-ends are stubbed and return HTTP 501 until wired). Ready for European Digital Identity Wallet integrations via the OIDC + OID4VP substrate.
Two URL shapes, one handler. Endpoints are dispatched on ?action=<path>. Every URL shown below in the shape POST /csc/v2/<segment> is rewritten by IIS to POST /csc.ashx?action=<segment>. Both forms are accepted. The prefix path is the one advertised by /csc/v2/info and is the shape spec-compliant CSC clients expect.
Auth. Except for info (open) and auth/login (which merely echoes back an already-validated Bearer), every endpoint requires an OIDC access token in the Authorization: Bearer <token> header. The Bearer is introspected against /oidc.ashx?action=introspect to resolve the acting sub; that value keys the per-user signing credential and appears as the sub claim inside every SAD JWT.
Common limits (host-level RASP). Bodies are capped at 32 KiB (10 MiB for signatures/signDoc). A per-IP rate bucket admits 30 requests per minute per action; over-limit returns 429 with Retry-After: 30. Non-HTTPS requests are rejected 403 (https_required). All hashes MUST be SHA-256 (32 raw bytes, base64-encoded); other digests return 400 only_sha256_supported.
CSC v2 §11.1 service metadata. Returns the supported spec version, the enumerated method list, the signing algorithm OIDs and the authentication type. Also reachable with GET for smoke tests.
Request
No parameters. Body ignored.
Response
{
"specs": "2.0.0.2",
"name": "Aloaha CodeB CSC Signing MVP",
"logo": "https://phone.aloaha.com/favicon.svg",
"region": "MT",
"lang": "en-US",
"description": "Cloud Signature Consortium v2 MVP -- AdES only, per-user signing credential (software-backed, not HSM). European Digital Identity Wallet integrations delivered via sign.html client. HSM-backed keys and QES via QSCD are outside the current phase.",
"authType": [ "oauth2code" ],
"oauth2": "https://phone.aloaha.com/oidc.ashx?action=authorize",
"methods": [
"auth/login", "auth/revoke",
"credentials/list", "credentials/info",
"credentials/authorize", "credentials/authorize/confirm",
"signatures/signHash", "signatures/signDoc", "signatures/timestamp"
],
"signAlgorithms": {
"algos": [ "1.2.840.10045.4.3.2", "1.2.840.113549.1.1.11" ]
},
"signature_qualifier_supported": [ "eu_eidas_aes" ]
}
Example
curl -X POST https://phone.aloaha.com/csc/v2/info
1.2.840.10045.4.3.2 = ecdsa-with-SHA256; 1.2.840.113549.1.1.11 = sha256WithRSAEncryption. The signature_qualifier_supported value eu_eidas_aes is CSC's identifier for Advanced Electronic Signature and matches the AdES-only scope disclosure above.
CSC v2 session-level token exchange. Accepts the OIDC access token in the Authorization header, introspects it against the CodeB OIDC provider to confirm the subject, and echoes the token back with a fixed expires_in. This makes CSC v2 clients that expect the specification's own login flow work without a second authorisation server.
Request headers
Authorization: Bearer <OIDC access token>
Request body
None — the token is taken from the header.
Response
{
"access_token": "<same token that was posted>",
"token_type": "Bearer",
"expires_in": 3600
}
Errors
401 invalid_token / bearer_required — header missing or not a Bearer.
401 invalid_token / introspect_failed — the token failed RFC 7662 introspection against the same tenant's OIDC provider.
Example
curl -X POST -H "Authorization: Bearer $ACCESS_TOKEN" \
https://phone.aloaha.com/csc/v2/auth/login
Revokes a previously issued session token. Delegates to /oidc.ashx?action=revoke. Returns { ok: true } unconditionally so revoke leaks no state about the token's prior validity.
Request body
{ "token": "<access token to revoke>" }
Response
{ "ok": true }
Errors
400 invalid_request / invalid_json — body is not JSON.
400 invalid_request / token_required — token field missing.
Lists the CSC credential IDs owned by the authenticated user. Under Phase 1b every user has a per-user EC P-256 credential, so the list has exactly one element. If the operator has set Csc:PerUserCerts=false, all users share the tenant-wide fallback credential.
Returns descriptor metadata for a specific credential: leaf certificate (base64 DER), Subject DN, Issuer DN, serial number, validity window, key algorithm OID + key length + curve OID, and module attributes. This is the same output CSC v2 defines in §11.4 with the CodeB-specific moduleType and moduleCertification attributes surfaced from the ICryptoModule boundary.
When the tenant has Csc:DefaultScal=2 the response also carries a PIN descriptor advertising that a Signature Activation Protocol second-factor is required:
Obtains a Signature Activation Data (SAD) — a short-lived JWT bound to the specific hash list the caller intends to sign. In SCAL1 the SAD is returned immediately; in SCAL2 a pending record is minted and the caller must complete a second-factor challenge via credentials/authorize/confirm. Selects between SCAL1 and SCAL2 via the tenant default (Csc:DefaultScal) or the per-request ?scal=1|2 query override.
The SAD is a compact JWS with payload claims iss, sub (= Bearer's introspected subject), aud=csc-sad, iat, exp (iat + 300 s), jti, credentialID, hash, hashAlgo, numSignatures, scal=1. Signed with ES256 by the tenant's SAD signing key when the credential is EC, or RS256 as an automatic fallback when the credential is RSA.
The webauthn_options object is present only when the user has previously registered a passkey; the client passes it verbatim to navigator.credentials.get() and posts the resulting assertion to credentials/authorize/confirm. The pending record TTL is 300 s.
SCAL2 second-factor confirmation. The client posts either a WebAuthn assertion (verified via CodeB.Passkeys.AuthenticationFacade per WebAuthn Level 2 — clientDataJSON type / origin / challenge match, rpIdHash, UP+UV flags, signature over authenticatorData || SHA-256(clientDataJSON), counter regression, one-shot session consume) or a PIN in either the JSON body or the X-CSC-Sign-PIN header (4–12 hex/digit characters).
{
"SAD": "eyJhbGciOiJFUzI1NiIs...<JWT>...",
"authorization_id": "<16-hex slug>",
"scal": "2",
"confirm_factor": "webauthn-hwk" /* or webauthn-swk, or pin */
}
On success the SAD carries an additional confirm_factor claim indicating whether the assertion was hardware-bound (webauthn-hwk), synced (webauthn-swk) or PIN-based (pin). Pending records are single-use and are deleted on both success and hard rejection.
400 webauthn_verification_failed — assertion failed a WebAuthn check; the specific reason is in the error_description.
403 invalid_token / user_mismatch — the Bearer's sub does not match the pending record's user.
404 invalid_request / authorization_not_found — unknown or already-consumed authorization ID.
For SCAL2 the RASP-side PIN filter enforces digits or hex, 4–12 characters. PINs are never written to logs. On WebAuthn hard-reject the pending record is deleted so an attacker cannot swap the assertion.
Signs one or more pre-computed SHA-256 hashes with the per-user credential. The caller assembles the CMS envelope client-side (as sign.html does for PAdES-B-B / B-T). The SAD must have been bound to exactly the hash list submitted here — count and content are verified constant-time.
{
"signatures": [ "<base64 raw signature bytes>" ]
}
The signatures[i] value is the raw signature over hash[i] under signAlgo: ECDSA r || s for ecdsa-with-SHA256, or the RSA signature primitive output for sha256WithRSAEncryption. Each successful signature is written to an append-only tenant audit journal keyed by sub, credentialID, hash and signature size.
CSC v2 §11.10 — server-side full-document signature. The caller ships one or more raw PDFs (base64) and CodeB does incremental-save PDF surgery, hashes the ByteRange, mints the CMS SignerInfo (with the same signed attributes as the browser flow: contentType, messageDigest, signingTime, signingCertificateV2 (RFC 5035 ESSCertIDv2 with issuerSerial), and RFC 6211cmsAlgorithmProtection), optionally fetches the RFC 3161 TimeStampToken and splices it as an id-aa-signatureTimeStampToken unsigned attribute (PAdES-B-T), and returns the fully-signed PDF.
Request body
{
"credentialID": "codeb-user-a1b2c3d4e5f60718",
"SAD": "<JWT from credentials/authorize with numSignatures ≥ documents.length>",
"documents": [
{
"document": "<base64 PDF bytes>",
"signature_format": "P",
"conformance_level": "AdES-B-T",
"signed_envelope_property": "Approval",
"container": "No",
"signAlgo": "1.2.840.10045.4.3.2",
"parameters": {
"signing_time": "2026-07-27T10:00:00Z",
"signing_reason": "I approve this contract",
"signing_location": "Valletta, MT",
"contact_info": "ada@example.org"
}
}
]
}
conformance_level: "AdES-B-B" (ETSI EN 319 142-1 baseline) or "AdES-B-T" (adds RFC 3161 signature-time-stamp).
signed_envelope_property: "Approval" only (certification signatures deferred).
container: "No" only (ASiC deferred).
signAlgo: 1.2.840.10045.4.3.2 (ECDSA-SHA256) or 1.2.840.113549.1.1.11 (RSA-SHA256).
parameters.signing_reason / signing_location / contact_info land in the PDF /Sig dict as /Reason / /Location / /ContactInfo.
MVP scope note — SAD hash-binding relaxed for signDoc.
Per CSC v2 §11.10, the Signature Activation Data (SAD) JWT should be bound to the specific hash(es) that will be signed. Because signDoc computes the document hashes on the server (the client uploads the whole PDF), the SAD cannot be pre-bound to those hashes at credentials/authorize time. This MVP relaxes that check: SADs are verified for signature validity, aud=csc-sad, exp / iat window, sub = Bearer subject, credentialID match, and numSignatures ≥ documents.length, but the specific hash list is not cross-checked. A future revision will implement the two-round-trip flow (upload → hash + challenge → authorize with those hashes → complete) for strict spec compliance. signHash continues to enforce full hash-binding as the spec requires. The relaxation is logged with [CSC-SIGNDOC-DIAG] SAD_ok (hash-binding relaxed for signDoc) so it appears in the audit trail.
400 unsupported_signature_format, unsupported_conformance_level, / only signed_envelope_property=Approval supported, / only container=No supported, / unsupported_signAlgo.
400 invalid_request / pdf_base64_invalid.
400 pdf_encrypted_unsupported — server-side signing of encrypted PDFs is deferred; the browser flow at sign.html supports Standard Security Handler V2 (RC4-128) and V4 (AES-128).
400 pdf_parse_error — xref / catalog / page structure could not be walked.
413 body_too_large — body exceeds the 10 MiB signDoc cap.
The full-document flow deliberately reuses the same on-disk credential and audit trail as signHash; the two endpoints are alternate transports for the same signing key.
RFC 3161 timestamp proxy. The server builds a TimeStampReq DER around a SHA-256 hash, posts it to the configured TSA, parses the TimeStampResp, extracts the TimeStampToken, and returns it base64-encoded. The browser signer (sign.html) splices this token into the CMS SignerInfo.unsignedAttrs as id-aa-signatureTimeStampToken, upgrading PAdES-B-B to PAdES-B-T.
TSA URL source
The TSA URL is read from the Windows registry at HKLM\SOFTWARE\CodeB, value TSAURL. If the value is empty or missing, the server writes back the default http://timestamp.sectigo.com/qualified and uses it. If the value is present but not a valid http[s]:// URL, the server returns 500 invalid_tsa_url instead of silently falling back — operator intervention is required.
Request body
{
"hash": "<base64 SHA-256 hash of the CMS SignerInfo signature bytes>",
"hashAlgo": "2.16.840.1.101.3.4.2.1",
"nonce": "0a1b2c3d4e5f60718" /* optional, hex; auto-generated when absent */
}
500 invalid_tsa_url — registry value present but not a URL. Operator must fix HKLM\SOFTWARE\CodeB\TSAURL.
502 tsa_upstream_failed — the TSA rejected the request, returned an empty body, or the TimeStampResp status was neither 0 (granted) nor 1 (granted-with-mods). The error_description carries the parsed reason (webexception, crash, empty, parse, status_<code>, no_token_after_status).
The browser signer writes a PDF signature dictionary with the following fields. When you use signHash, these are entirely under client control because the client assembles the whole envelope. When you use signDoc, the server assembles the dictionary; the caller supplies the values through parameters.*.
/Filter /Adobe.PPKLite and /SubFilter /adbe.pkcs7.detached — produces the etsi.CAdES.detached CMS profile Adobe Reader accepts as PAdES-B-B (the PAdES-only/ETSI.CAdES.detached SubFilter is available under a build flag but the default is the Adobe-compatible form).
/Reason — client-controlled (signHash) or from parameters.signing_reason (signDoc).
/M — UTC signing timestamp in PDF D:YYYYMMDDHHMMSSZ form. Server-set on signDoc (from parameters.signing_time when provided, else DateTime.UtcNow).
/Name — pending: currently derived server-side from the Subject DN Common Name. A future release will expose it as parameters.signer_name.
/ByteRange — the four-integer array covering the entire PDF except the placeholder hex window (brackets included in the hole). Filled after CMS assembly so the ByteRange itself is inside the signed range.