Security
:::info Source
Sourced from services/content-service/SECURITY_MODEL.md in the documentation repo.
:::
1. Authentication
- JWT from identity-service (CF);
tidclaim required on all endpoints except public verification. - S2S: API keys for worker ingress (SCORM import pipeline, internal orchestrations).
- Webhooks from SCORM import partners: HMAC-SHA256 + 5-min timestamp.
2. Authorization
- RBAC/ABAC via tenant-service policy bundle.
- Resources:
content:package,content:bundle,content:export,content:import. - Required roles:
content:package:build→ author/publisher (owner tenant) or platform_admin.content:bundle:create→ internal (triggered byenrollment.created.v1).content:bundle:download→ authenticated user with matching enrollment + device binding.content:package:revoke→ provider_admin, platform_admin.content:export:scorm|html|xapi→ author/publisher.
3. Multi-Tenant Enforcement
| Layer | Rule |
|---|---|
| API | JWT tid must match X-Tenant-Id header; 403 authz.tenant_not_a_member on mismatch |
| Domain | PlayPackage.tenantId, PlayPackageBundle.tenantId, LicenseEnvelope all anchored to TenantId VO |
| Postgres | RLS USING (tenant_id = current_setting('app.tenant_id')::uuid) on every table |
| Storage (S3) | Per-tenant prefix tenants/{tid}/play-packages/..., tenants/{tid}/bundles/...; bucket policy denies cross-prefix access |
| Signed URLs | Scoped per-bundle + per-caller-device; 10-min TTL |
4. Data Classification & Encryption
| Artifact | Classification | At-rest | In-transit |
|---|---|---|---|
| PlayPackage (unencrypted) | Confidential | AES-256 KMS-shared | TLS 1.3 |
| PlayPackage Bundle | Restricted (offline-bundled) | AES-256-GCM per-device-derived key | TLS 1.3 |
| LicenseEnvelope | Restricted | AES-256 + signed JWS | TLS 1.3 |
| SCORM import staging | Internal | AES-256 | TLS 1.3 + mTLS internal |
| Answer keys (inside package) | Restricted | AES-256 with separate KEK | TLS 1.3 |
4.1 Bundle Encryption Key Derivation
DEK = HKDF(
master = KMS-wrapped tenant signing key,
info = "bundle|" || bundleId || "|device|" || devicePublicKeyHash,
length = 32
)
# Encrypt bundle content with AES-256-GCM(DEK, iv = random 96-bit)
- DEK derivable only by server (with KMS) and device (with its private key counterpart).
- Device private key never leaves device (secure enclave / Keystore).
- Bundle tamper → signature verification fails → unmount +
content.bundle.tamper_detected.v1.
5. LicenseEnvelope Enforcement (Offline)
Bundle mount steps (player → content-service SDK):
- Verify bundle SHA-256 matches manifest.
- Verify JWS signature (tenant signing key; cached in device).
- Verify
LicenseEnvelope.signature. - Verify
expiresAt > now. - Verify
deviceIdmatches bound device. - Enforce feature flags:
aiTutor,assessments,certificate,copyDownloadable. - Mount read-only; decrypt asset blobs on-demand.
Failure at any step → unmount + diagnostic event.
5.1 Revocation
content.play_package.bundle.revoked.v1propagates via sync → device unmounts within 60s online.- Offline devices: license expires at
expiresAt; cannot be extended offline.
6. SCORM Import Sandbox
- Imports run in isolated worker (seccomp + AppArmor; no network; memory cap; CPU cap; 5-min timeout).
- Manifest (
imsmanifest.xml) validated against XSD before extraction. - No JS execution during import; all content treated as data.
- Signed origin allowlist for referenced external resources.
- Imports quarantined until AV + content-safety scan passes.
7. JWS Signing
- Tenant signing keys: HSM-backed; rotated annually or on compromise.
- Signing algorithm: EdDSA Ed25519 (identity-parity) or ES256 (wider client compat). Selected per tenant.
- Signatures embed
kid; rotation overlap ≥ 2 days. - Public verification keys distributed via
/.well-known/content-keys.json(CDN-cached).
8. Audit Logging
Append-only audit entries for:
content.play_package.built/.revokedcontent.play_package.bundle.published/.revoked- License envelope issuance
- SCORM import completion
- Tamper detection events
Daily Merkle-anchored root emitted as audit.merkle.anchored.v1.
9. Threat Model
| Threat | Mitigation |
|---|---|
| Bundle piracy (share across devices) | Per-device key derivation; device binding |
| Bundle tamper | SHA-256 + JWS signature verification on mount |
| License forgery | JWS signed by tenant HSM key; device verifies |
| SCORM RCE | Sandbox + manifest validation + no eval |
| Cross-tenant bundle access | Tenant prefix + signed URL scope |
| Replay attack on signed URL | Short TTL (10 min); single-use nonce |
| Revocation bypass (offline) | expiresAt cap; tamper flag persists; next online sync unmounts |
| AI tutor context leak (via assistant config in manifest) | Per-tenant config; no cross-tenant RAG |
10. Compliance
- Part of GDPR erasure saga participation: delete packages, bundles, envelopes tied to erased user.
- Certificates (in completed courses) retained per tenant policy (may require legal hold).
- EU AI Act: assistant config provenance tracked.