Trust anchor pinning
Pin the Root CA by its HashedId8, not by URL.
# Root CA certificate (COER) curl https://pki.skyv2x.com/trustanchor -o root.cert # HashedId8 = last 8 bytes of SHA-256 (SHA-384 for Brainpool P-384 certificates, such as the TLM) openssl dgst -sha256 -binary root.cert | tail -c 8 | xxd -p -u # → D5CC48BA56F87FCA
The same value is published in /capabilities under trust_anchors.root_ca.hashedId8, next to the TLM and MA.
Enrolment — get an EC
The EA must know the station's canonical key and permissions before the first request, and only a subscriber (a manufacturer or operator) registers stations. For testing, a sandbox token needs no approval:
curl -X POST https://pki.skyv2x.com/subscribers/sandbox/token # → {"subscriber_id": "sandbox-…", "token": "cst_…", "max_profile": {…}} curl -X POST https://pki.skyv2x.com/subscribers/me/stations \ -H 'Authorization: Bearer cst_…' -H 'Content-Type: application/json' \ -d '{"canonical_public_key": "02…", "curve": "ecdsaNistP256"}' # → {"its_id": "b2f42bd8e5bdde59", "app_permissions": {"36": "01fffc", …}} # optional: "its_id" to name the station, "app_permissions" to narrow the profile, "public_key_pem" instead of hex
Organisations register at POST /subscribers and receive their token and maximum profile after review.
- Outer: ECIES to the EA encryption key, wrapping a
SignedDatasigned by the canonical key (signerself). - Inner:
SignedData(InnerEcRequest)signed by the new verification key (proof of possession). - Refusals come back encrypted:
unknownits(not registered),invalidsignature(outer key ≠ registered key),deniedpermissions.
python3 its_client.py --server https://pki.skyv2x.com # [2/4] sandbox subscriber · station registered # [3/4] EC issued · HashedId8 f63341bcf0e4f7c7 · 216 B · validity 8760 h
Authorization — get an AT
With an EC, request Authorization Tickets from the AA. The AA only sees the SharedAtRequest and an HMAC keyTag; the EA validates the EC and confirms the permissions: those requested, intersected with the station's profile.
# [4/4] AT issued · HashedId8 0e66d4c7df963d9a · 161 B · subject none · validity 168 h # PSIDs 36 (CAM) · 37 (DENM) · 141 (GN-MGMT) · 1618 (MR)
One AT per request; the pool and its rotation live in the ITS-S.
Subset chain audit — /conformance
The chain audited the way a strict receiver does it: every AT through its AA to the Root CA, plus the EA, MA and TLM edges.
curl https://pki.skyv2x.com/conformance | jq '{ rca: .rca.hashed_id8, all_checks_ok, real_gaps_count, ssp_real_fails_count, checks: [.chain_checks[] | {child: .child_name, ok, certissue_gaps, appperm_gaps}] }'
real_gaps_count— PSIDs a certificate claims that its issuer does not delegate. Target0.ssp_real_fails_count— SSP values outside the issuer's range. Target0.expected_gaps_by_design,ssp_known_fails— declared exceptions. Both empty.
Verify a signed CAM
Verifying a CAM means AT → AA → Root CA. The CTL lists the EAs and AAs under our Root CA; the ECTL, signed by the TLM (Brainpool P-384), lists the Root CAs.
# ECTL, signed by the TLM curl https://pki.skyv2x.com/getectl -o ectl.bin # CTL and CRL of our Root CA RCA_HID=$(openssl dgst -sha256 -binary root.cert | tail -c 8 | xxd -p -u) curl https://pki.skyv2x.com/getctl/$RCA_HID -o rca-ctl.bin curl https://pki.skyv2x.com/getcrl/$RCA_HID -o crl.bin
Testbed refresh windows: ECTL 90 days, CTL 30, CRL 7.
Python end-to-end client
The endpoints are plain HTTPS. ECIES, the HMAC keyTag and OER encoding belong to your stack.
import requests, hashlib BASE = "https://pki.skyv2x.com" # 1. Trust anchor rca = requests.get(f"{BASE}/trustanchor").content rca_hid8 = hashlib.sha256(rca).digest()[-8:].hex() # 2. Discovery caps = requests.get(f"{BASE}/capabilities").json() assert rca_hid8.upper() == caps["trust_anchors"]["root_ca"]["hashedId8"] # 3. Enrolment and authorization: ECIES + keyTag + OER (TS 102 941 Annex C)
Misbehaviour reporting
Encrypt the Misbehaviour Report to the MA encryption key and POST it. Castell verifies the reporter's AT (PSID 1618), the signature and the freshness window; forged or replayed reports are rejected.
# MA certificate (carries the encryption key) curl https://pki.skyv2x.com/ma -o ma.cert # Report signed by the AT, encrypted to the MA (ECIES + AES-CCM) curl -X POST -H "Content-Type: application/x-its-mr" \ --data-binary @mr.bin \ https://pki.skyv2x.com/mr # → 202, report_id in the body
Standard upload paths: POST /uploadMR-v1, /uploadMR-v1/Signed, /uploadMR-v1/SignedAndEncrypted (empty-body responses). /mr returns the decoded summary.