Trust anchor pinning
Every V2X stack needs a root of trust. Pin the SkyV2X RCA by its HashedId8 and SHA-256, never by URL alone.
# Fetch the RCA cert (COER, TS 103 097 v1.3.1) curl https://pki.skyv2x.com/trustanchor -o root.cert # Verify HashedId8 — the canonical identity (TS 103 097) openssl dgst -sha256 -binary root.cert | tail -c 8 | xxd -u -c 8 # → 8E0674D074994268
In production C-ITS, an OEM ships its devices with the trust anchor burned into a TPM or HSM. For early integration testing, file-based pinning is acceptable.
Enrolment — get an EC
An ITS-S authenticates itself to the EA (Enrolment Authority) with its canonical key, and receives an Enrolment Credential (EC) tied to that canonical identity.
Wire format per TS 102 941.2:
- Outer:
EciesP256EncryptedKeytargeting EA encryption pubkey - Inner:
SignedData(InnerEcRequest)signed by the canonical key - Payload:
InnerEcRequest { itsId, certificateFormat, publicKeys, requestedSubjectAttributes }
Quickest path — use a reference Python client (see Python section below for the minimal version):
python3 its_client.py --server https://pki.skyv2x.com # Output (truncated): # [3/4] EnrolmentRequest → EC issued # ✓ EC HashedId8 = 4fdb002bb5e37c98 # ✓ EC issuer = sha256AndDigest(EA HashedId8) # ✓ EC subject = name = canonical_id_hex
Authorization — get an AT
With an EC, request Authorization Tickets (AT) from the AA. ATs are pseudonymous — the AA cannot link the AT back to the EC (privacy boundary per TS 102 941.3).
Privacy guarantee: EC.publicKey stays inside an envelope encrypted to EA. AA only sees the SharedAtRequest + an HMAC keyTag. AA forwards to EA for validation, never sees the EC.
# The reference client does both EC and AT in one run: # [4/4] AuthorizationRequest → AT issued # ✓ AT HashedId8 = 2468d5f305e35cc6 # ✓ AT subject = none (privacy) # ✓ AT validity = ('hours', 8760) # ✓ PSIDs = [36 (CAM), 37 (DENM), 141 (GN-MGMT)]
A real ITS-S maintains a pool of ~20 ATs rotating every ~5 min for unlinkability. Our testbed issues 1 AT per request; rotation logic belongs in the ITS-S, not the PKI.
Subset chain audit — /conformance
Castell publishes its subset chain audit as a public endpoint. Apply ultra-strict per IEEE 1609.2 §5.1.2 — recursive from each AT through its AA to the RCA, plus EA and MA edges.
curl https://pki.skyv2x.com/conformance | jq '{ rca: .rca.hashed_id8, all_checks_ok, real_gaps_count, expected_gaps_by_design, checks: [.chain_checks[] | {child: .child_name, ok, certissue_gaps, appperm_gaps}] }'
Response fields:
real_gaps_count— gaps that require fixing. Target:0.expected_gaps_by_design— edges that are gaps by spec (e.g.ma.cert: MA is a separate service per TS 103 759 §8.2, not delegated through RCAcertIssuePermissions).chain_checks[].appperm_gaps/certissue_gaps— PSIDs the child claims that the issuer does not authorize.
Receiver discipline: if your stack only validates AT → AA and skips the AA → RCA edge, it will accept frames that a fully compliant validator rejects. Castell's contract: real_gaps_count == 0 means no AT in our pool will be rejected by a recursive-strict validator on permission grounds.
Verify a signed CAM
Any V2X stack receiving a CAM signed by an AT must verify the chain AT → AA → RCA. The CTL gives you the AAs you trust under our RCA.
# 1. ECTL (TLM-signed) — list of trusted RCAs curl https://pki.skyv2x.com/getectl -o ectl.bin # 2. RCA-CTL — list of trusted EAs/AAs under our RCA RCA_HID=$(openssl dgst -sha256 -binary root.cert | tail -c 8 | xxd -p) curl https://pki.skyv2x.com/getctl/$RCA_HID -o rca-ctl.bin # 3. CRL — revoked sub-CAs curl https://pki.skyv2x.com/getcrl/$RCA_HID -o crl.bin
Refresh ECTL every 90 days, RCA-CTL every 30 days, CRL every 7 days (testbed defaults — production CCMS uses shorter windows).
Python end-to-end client
Minimal client using any HTTP library plus your ASN.1 OER library of choice. The sketch below uses pure stdlib for the wire calls; ECIES, HMAC keyTag and OER encoding are deliberately left to your stack.
import requests, hashlib from cryptography.hazmat.primitives.asymmetric import ec 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 — capabilities JSON caps = requests.get(f"{BASE}/capabilities").json() assert rca_hid8.upper() == caps["trust_anchors"]["root_ca"]["hashedId8"] # 3. Enrolment — full EC + AT flow needs ECIES + HMAC keyTag # + ASN.1 OER encoding. Wire format and field-level spec in # TS 102 941 Annex C.
The endpoints above are stable. The client-side cryptography (ECIES wrapper, HMAC keyTag, OER encoding) is what you implement — Castell only verifies signatures and decrypts requests on the server side.
Misbehaviour reporting
If your ITS-S detects misbehaviour (TS 103 759 detectors), encrypt the Misbehaviour Report (MR) to the MA pubkey and POST it.
# 1. Get the MA cert (includes encryption pubkey) curl https://pki.skyv2x.com/ma -o ma.cert # 2. Build MR per TS 103 759 (Asr* schemas from ETSI Forge) # 3. Encrypt to MA pubkey (ECIES + AES-CCM) # 4. POST curl -X POST -H "Content-Type: application/x-its-mr" \ --data-binary @mr.bin \ https://pki.skyv2x.com/mr # Response: 202 Accepted, report_id = SHA-256(payload)[:16]
v0.1 testbed validates only that the MR decrypts correctly. Full TS 103 759 ASN.1 parse (AsrCam / AsrDenm) ships in v0.2.