Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/shell-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ on:
- 'tools/display_built_in_profiles.py'
- 'tools/display_visual_pack_providers.py'
- 'tools/validate_content_identity_policy.py'
- 'tools/validate_save_sync_contract.py'
- 'tools/validate_libretro_artwork_policy.py'
- 'tools/ci-scope.sh'
- 'tools/bloom-device'
Expand All @@ -48,6 +49,7 @@ on:
- 'build/display-visual-pack-providers.json'
- 'static/build/.tmp_update/res/bloom/visual-packs/**'
- 'build/content-identity-policy.json'
- 'build/save-sync-*.json'
- 'build/libretro-artwork-policy.json'
- 'build/ra-core-policy.json'
- 'build/dependencies.lock'
Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,12 @@ package-release-unsigned:
--policy $(ROOT_DIR)/build/save-migration-policy.json \
--schema $(ROOT_DIR)/build/save-migration-policy.schema.json \
--core-policy $(ROOT_DIR)/build/core-selection-policy.json
@python3 $(ROOT_DIR)/tools/validate_save_sync_contract.py \
--contract $(ROOT_DIR)/build/save-sync-contract.json \
--contract-schema $(ROOT_DIR)/build/save-sync-contract.schema.json \
--bundle-schema $(ROOT_DIR)/build/save-sync-bundle.schema.json \
--core-policy $(ROOT_DIR)/build/core-selection-policy.json \
--content-policy $(ROOT_DIR)/build/content-identity-policy.json
@python3 $(ROOT_DIR)/tools/validate_transfer_policy.py \
--policy $(ROOT_DIR)/build/transfer-policy.json \
--schema $(ROOT_DIR)/build/transfer-policy.schema.json
Expand Down
124 changes: 124 additions & 0 deletions build/save-sync-bundle.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bloomos.dev/schemas/save-sync-bundle-v1.json",
"title": "BloomOS authenticated immutable save bundle envelope",
"type": "object",
"additionalProperties": false,
"required": ["schema", "bundle_id", "manifest", "authentication"],
"properties": {
"schema": {"const": 1},
"bundle_id": {"type": "string", "pattern": "^bloom-save-bundle-v1:[0-9a-f]{64}$"},
"manifest": {"$ref": "#/$defs/manifest"},
"authentication": {"$ref": "#/$defs/authentication"}
},
"$defs": {
"safeId": {"type": "string", "maxLength": 96, "pattern": "^[a-z0-9][a-z0-9._-]*$"},
"sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
"gameId": {"type": "string", "pattern": "^bloom-game-v1:[0-9a-f]{64}$"},
"contentIdentity": {"type": "string", "maxLength": 160, "pattern": "^bloom-content:[a-z][a-z0-9-]{0,63}:v[1-9][0-9]*:[0-9a-f]{64}$"},
"deviceId": {"type": "string", "pattern": "^bloom-device-v1:[0-9a-f]{32}$"},
"keyId": {"type": "string", "pattern": "^bloom-sync-key-v1:[0-9a-f]{64}$"},
"lineageId": {"type": "string", "pattern": "^bloom-save-lineage-v1:[0-9a-f]{32}$"},
"softwareId": {"type": "string", "maxLength": 64, "pattern": "^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$"},
"backend": {
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": ["emulator_id", "core_id", "component_id", "component_sha256"],
"properties": {
"emulator_id": {"type": "string", "pattern": "^libretro:[a-z0-9][a-z0-9._-]*$"},
"core_id": {"$ref": "#/$defs/safeId"},
"component_id": {"$ref": "#/$defs/safeId"},
"component_sha256": {"$ref": "#/$defs/sha256"}
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["emulator_id", "component_id", "component_sha256"],
"properties": {
"emulator_id": {"type": "string", "pattern": "^standalone:[a-z0-9][a-z0-9._-]*$"},
"component_id": {"$ref": "#/$defs/safeId"},
"component_sha256": {"$ref": "#/$defs/sha256"}
}
}
]
},
"sourceDevice": {
"type": "object",
"additionalProperties": false,
"required": ["device_id", "key_epoch"],
"properties": {
"device_id": {"$ref": "#/$defs/deviceId"},
"key_epoch": {"type": "integer", "minimum": 1, "maximum": 2147483647}
}
},
"payload": {
"type": "object",
"additionalProperties": false,
"required": ["format", "sha256", "bytes"],
"properties": {
"format": {"const": "bloom-save-payload-v1"},
"sha256": {"$ref": "#/$defs/sha256"},
"bytes": {"type": "integer", "minimum": 0, "maximum": 67108864}
}
},
"software": {
"type": "object",
"additionalProperties": false,
"required": ["bloomos_version", "source_commit"],
"properties": {
"bloomos_version": {"$ref": "#/$defs/softwareId"},
"source_commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}
}
},
"manifest": {
"type": "object",
"additionalProperties": false,
"required": [
"schema",
"bloom_game_id",
"content_identity",
"system_id",
"save_kind",
"compatibility_id",
"backend",
"source_device",
"lineage_id",
"generation",
"parent_payload_sha256",
"payload",
"software"
],
"properties": {
"schema": {"const": 1},
"bloom_game_id": {"$ref": "#/$defs/gameId"},
"content_identity": {"$ref": "#/$defs/contentIdentity"},
"system_id": {"$ref": "#/$defs/safeId"},
"save_kind": {"enum": ["save", "state"]},
"compatibility_id": {"$ref": "#/$defs/safeId"},
"backend": {"$ref": "#/$defs/backend"},
"source_device": {"$ref": "#/$defs/sourceDevice"},
"lineage_id": {"$ref": "#/$defs/lineageId"},
"generation": {"type": "integer", "minimum": 1, "maximum": 9007199254740991},
"parent_payload_sha256": {"type": "string", "pattern": "^(none|[0-9a-f]{64})$"},
"payload": {"$ref": "#/$defs/payload"},
"created_at": {"type": "string", "maxLength": 32, "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"},
"software": {"$ref": "#/$defs/software"}
}
},
"authentication": {
"type": "object",
"additionalProperties": false,
"required": ["scheme", "key_id", "key_epoch", "signed_bytes", "signature"],
"properties": {
"scheme": {"const": "ed25519-rfc8032"},
"key_id": {"$ref": "#/$defs/keyId"},
"key_epoch": {"type": "integer", "minimum": 1, "maximum": 2147483647},
"signed_bytes": {"const": "rfc8785-jcs-manifest-v1"},
"signature": {"type": "string", "pattern": "^[A-Za-z0-9_-]{86}$"}
}
}
}
}
127 changes: 127 additions & 0 deletions build/save-sync-contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
{
"schema": 1,
"contract_id": "bloomos-save-sync-contract-v1",
"bundle_schema_id": "https://bloomos.dev/schemas/save-sync-bundle-v1.json",
"limits": {
"max_manifest_bytes": 16384,
"max_payload_bytes": 67108864,
"max_generation": 9007199254740991,
"max_identifier_bytes": 160,
"max_retained_ancestry": 64
},
"identity": {
"game_id_authority": "bloom-game-v1",
"content_identity_authority": "bloom-content-identity",
"bundle_id_format": "bloom-save-bundle-v1:<lowercase-sha256>",
"bundle_id_digest": "sha256(rfc8785-jcs(manifest))",
"device_id_format": "bloom-device-v1:<32-lowercase-hex>",
"device_id_semantics": "stable-random-128-bit-identity-independent-of-key",
"lineage_id_format": "bloom-save-lineage-v1:<32-lowercase-hex>",
"lineage_id_semantics": "stable-random-128-bit-identity-per-logical-save-lineage",
"first_generation": 1,
"first_parent": "none",
"later_parent": "previous-payload-sha256",
"wall_time": "optional-advisory-only-never-orders"
},
"authentication": {
"signature_algorithm": "ed25519-rfc8032",
"canonicalization": "rfc8785-jcs",
"signed_object": "manifest",
"signature_encoding": "base64url-no-padding",
"key_id_format": "bloom-sync-key-v1:<sha256-of-raw-ed25519-public-key>",
"verification_order": [
"bounds",
"schema",
"bundle-id",
"paired-device",
"active-key-epoch",
"signature",
"compatibility",
"conflict"
]
},
"pairing": {
"authorization": "paired-device-and-active-key-epoch-required",
"deauthorization": "reject-new-bundles-preserve-existing-user-data",
"rotation": "new-key-epoch-requires-old-and-new-signatures-or-local-physical-repair",
"cloned_device": "same-device-id-with-divergent-active-key-or-lineage-history-requires-repair",
"replay": "exact-bundle-and-payload-is-idempotent-any-id-collision-is-security-event"
},
"invariants": {
"bundle_id": "equals-sha256-of-rfc8785-jcs-manifest",
"key_epoch": "authentication-key-epoch-equals-manifest-source-device-key-epoch",
"generation_parent": "generation-one-parent-none-otherwise-previous-payload-sha256",
"compatibility": "save-uses-save-compatibility-state-uses-state-compatibility",
"signature": "verifies-with-active-paired-key-id"
},
"conflicts": {
"comparison_order": [
"validity",
"authorization",
"bundle_id",
"game_and_content_identity",
"system_and_save_kind",
"compatibility_id",
"lineage_id",
"generation",
"parent_payload_sha256",
"payload_sha256"
],
"timestamp_role": "advisory-only-never-orders",
"unknown_history": "preserve-incoming-and-require-explicit-review",
"outcomes": [
{
"id": "new_lineage",
"condition": "no-current-bundle-for-identity-and-lineage",
"action": "stage-candidate-only"
},
{
"id": "exact_replay",
"condition": "same-bundle-id-and-payload-sha256",
"action": "idempotent-noop"
},
{
"id": "bundle_id_collision",
"condition": "same-bundle-id-with-different-canonical-manifest-or-payload",
"action": "reject-and-record-security-event"
},
{
"id": "incompatible",
"condition": "game-content-system-kind-or-compatibility-mismatch",
"action": "preserve-both-and-require-explicit-review"
},
{
"id": "fast_forward",
"condition": "same-lineage-next-generation-and-parent-equals-current-payload",
"action": "stage-candidate-only"
},
{
"id": "stale_ancestor",
"condition": "same-lineage-older-generation-and-payload-is-retained-verified-ancestor",
"action": "idempotent-noop"
},
{
"id": "fork",
"condition": "same-lineage-divergent-generation-or-parent",
"action": "preserve-both-and-require-explicit-review"
},
{
"id": "history_required",
"condition": "generation-gap-or-unverified-older-generation",
"action": "preserve-incoming-and-request-contiguous-history"
},
{
"id": "unrelated_lineage",
"condition": "same-game-save-identity-with-different-lineage",
"action": "preserve-both-and-require-explicit-review"
}
]
},
"isolation": {
"transport_dependency": "none",
"live_save_writes": "forbidden",
"generic_transfer_access": "read-only",
"publication_owner": "sync02-transaction-only",
"failure": "preserve-user-data-and-fail-closed"
}
}
Loading
Loading