From 31b68cb0f1819d8ae25588897a577630b67f7692 Mon Sep 17 00:00:00 2001 From: "David E. Wheeler" Date: Tue, 8 Oct 2024 17:39:29 +0200 Subject: [PATCH] Add certs, JWS, payload schemas Following [RFC 5], add new JSON schemas for a `certs` property containing JWS [JSON Serialization], supporting both the general and flattened syntaxes. The schemas are: * `certs.schema.json`: One or more certifications, with the `pgxn` property required. * `jws.schema.json`: JWS general and flattened [JSON Serialization] * `jws-header.schema.json`: JWS headers * `jwk.schema.json`: [RFC 7517] JSON Web Key (JWK) format, required by the `jwk` property of `jws-header.schema.json` * `payload.schema.json`: The PGXN release payload Include tests for each of these schemas. [RFC 5]: https://github.com/pgxn/rfcs/pull/5 [JSON Serialization]: https://datatracker.ietf.org/doc/html/rfc7515#section-7 [RFC 7517]: https://datatracker.ietf.org/doc/html/rfc7517 --- schema/v2/certs.schema.json | 13 + schema/v2/jwk.schema.json | 74 ++ schema/v2/jws-header.schema.json | 61 ++ schema/v2/jws.schema.json | 91 ++ schema/v2/payload.schema.json | 54 ++ src/tests/v2.rs | 1419 +++++++++++++++++++++++++++++- 6 files changed, 1684 insertions(+), 28 deletions(-) create mode 100644 schema/v2/certs.schema.json create mode 100644 schema/v2/jwk.schema.json create mode 100644 schema/v2/jws-header.schema.json create mode 100644 schema/v2/jws.schema.json create mode 100644 schema/v2/payload.schema.json diff --git a/schema/v2/certs.schema.json b/schema/v2/certs.schema.json new file mode 100644 index 0000000..579cfce --- /dev/null +++ b/schema/v2/certs.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/certs.schema.json", + "title": "Certifications", + "description": "One or more cryptographic signatures or certifications that attest to the authenticity or other characteristics of a distribution release.", + "type": "object", + "properties": { + "pgxn": { "$ref": "jws.schema.json" } + }, + "patternProperties": { "^[xX]_.": { "description": "Custom key" } }, + "additionalProperties": false, + "required": ["pgxn"] +} diff --git a/schema/v2/jwk.schema.json b/schema/v2/jwk.schema.json new file mode 100644 index 0000000..7b42e8c --- /dev/null +++ b/schema/v2/jwk.schema.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/jwk.schema.json", + "title": "JSON Web Key", + "description": "[RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517) JSON Web Key (JWK) format. Supports both the general and flattened syntaxes.", + "type": "object", + "properties": { + "kty": { + "type": "string", + "description": "Key Type: identifies the cryptographic algorithm family used with the key, such as “RSA” or “EC”." + }, + "use": { + "type": "string", + "description": "Public Key Use: identifies the intended use of the public key — encrypting data (“enc”) or verifying the signature on data (“sig”)." + }, + "key_ops": { + "type": "array", + "minItems": 1, + "items": { "type": "string" }, + "description": "Key Operations: identifies the operation(s) for which the key is intended to be used, and intended for use cases in which public, private, or symmetric keys may be present." + }, + "alg": { + "type": "string", + "description": "Algorithm: identifies the algorithm intended for use with the key." + }, + "kid": { + "type": "string", + "description": "Key ID: used to match a specific key." + }, + "x5u": { + "type": "string", + "format": "uri", + "description": "X.509 URL: a URI that refers to a resource for an X.509 public key certificate or certificate chain" + }, + "x5c": { + "type": "array", + "description": "X.509 Certificate Chain: contains a chain of one or more PKIX certificates", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "description": "Base 64-encoded DER PKIX certificate value." + } + }, + "x5t": { + "type": "string", + "pattern": "^[A-Za-z0-9-_]{12,}$", + "description": "X.509 Certificate SHA-1 Thumbprint: Base 64 URL-encoded SHA-1 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate." + }, + "x5t#S256": { + "type": "string", + "pattern": "^[A-Za-z0-9-_]{12,}$", + "description": "X.509 Certificate SHA-256 Thumbprint: Base 64 URL-encoded SHA-256 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate." + } + }, + "required": ["kty"], + "examples": [ + { + "kty": "EC", + "crv": "P-256", + "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "use": "enc", + "kid": "1" + }, + { + "kty": "RSA", + "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + "e": "AQAB", + "alg": "RS256", + "kid": "2011-04-29" + } + ] +} diff --git a/schema/v2/jws-header.schema.json b/schema/v2/jws-header.schema.json new file mode 100644 index 0000000..fccec2c --- /dev/null +++ b/schema/v2/jws-header.schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/jws-header.schema.json", + "title": "JWS JOSE Header", + "description": "[RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515) JSON Web Signature (JWS) [Header](https://datatracker.ietf.org/doc/html/rfc7515#section-4) format, describing the digital signature or MAC applied to the JWS Protected Header and the JWS Payload and optionally additional properties of the JWS.", + "type": "object", + "properties": { + "alg": { + "type": "string", + "description": "Algorithm: identifies the cryptographic algorithm used to secure the JWS." + }, + "jku": { + "type": "string", + "format": "uri", + "description": "JWK Set URL: a URI that refers to a resource for a set of JSON-encoded public keys, one of which corresponds to the key used to digitally sign the JWS." + }, + "jwk": { + "$ref": "jwk.schema.json", + "description": "JSON Web Key: the public key that corresponds to the key used to digitally sign the JWS, formatted as a JSON Web Key (JWK)." + }, + "kid": { + "type": "string", + "description": "Key ID: a hint indicating which key was used to secure the JWS." + }, + "x5u": { + "type": "string", + "format": "uri", + "description": "X.509 URL: a URI that refers to a resource for the X.509 public key certificate or certificate chain corresponding to the key used to digitally sign the JWS." + }, + "x5c": { + "type": "array", + "description": "X.509 Certificate Chain: the X.509 public key certificate or certificate chain [RFC5280] corresponding to the key used to digitally sign the JWS.", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "description": "Base 64-encoded DER PKIX certificate value." + } + }, + "x5t": { + "type": "string", + "pattern": "^[A-Za-z0-9-_]{12,}$", + "description": "X.509 Certificate SHA-1 Thumbprint: Base 64 URL-encoded SHA-1 thumbprint (a.k.a. digest) of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign the JWS." + }, + "x5t#S256": { + "type": "string", + "pattern": "^[A-Za-z0-9-_]{12,}$", + "description": "X.509 Certificate SHA-256 Thumbprint: Base 64 URL-encoded SHA-256 thumbprint (a.k.a. digest) of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign the JWS." + }, + "typ": { + "type": "string", + "description": "Type: used by JWS applications to declare the media type of this complete JWS." + }, + "cty": { + "type": "string", + "description": "Content Type: used by JWS applications to declare the media type [IANA.MediaTypes](https://datatracker.ietf.org/doc/html/rfc7515#ref-IANA.MediaTypes) of the secured content (the payload)." + } + }, + "minProperties": 1, + "examples": [{ "kid": "2010-12-29" }, { "typ": "JWT", "alg": "HS256" }] +} diff --git a/schema/v2/jws.schema.json b/schema/v2/jws.schema.json new file mode 100644 index 0000000..e34028e --- /dev/null +++ b/schema/v2/jws.schema.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/jws.schema.json", + "title": "JWS JSON Serialization", + "description": "[RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515) JSON Web Signature (JWS) [JSON Serialization](https://datatracker.ietf.org/doc/html/rfc7515#section-7.2). Supports both the general and flattened syntaxes.", + "type": "object", + "oneOf": [ + { + "$comment": "[General JWS JSON Serialization Syntax](https://datatracker.ietf.org/doc/html/rfc7515#section-7.2.1)", + "properties": { + "payload": { "$ref": "#/$defs/payload" }, + "signatures": { + "type": "array", + "description": "Encoded JWS Signature values", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "protected": { "$ref": "#/$defs/protected" }, + "header": { "$ref": "jws-header.schema.json" }, + "signature": { "$ref": "#/$defs/signature" } + }, + "required": ["signature"] + } + } + }, + "required": ["payload", "signatures"], + "additionalProperties": true + }, + { + "$comment": "[Flattened JWS JSON Serialization Syntax](https://datatracker.ietf.org/doc/html/rfc7515#section-7.2.2)", + "properties": { + "payload": { "$ref": "#/$defs/payload" }, + "protected": { "$ref": "#/$defs/protected" }, + "header": { "$ref": "jws-header.schema.json" }, + "signature": { "$ref": "#/$defs/signature" } + }, + "required": ["payload", "signature"], + "additionalProperties": true + } + ], + "$comment": "Additional members can be present in both the JSON objects defined above; if not understood by implementations encountering them, they MUST be ignored.", + "examples": [ + { + "protected": "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9", + "payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ", + "signature": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" + }, + { + "protected": "eyJhbGciOiJSUzI1NiJ9", + "payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ", + "signature": "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-r7t1dnZcAcQjbKBYNX4BAynRFdiuBLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw" + }, + { + "payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ", + "signatures": [ + { + "protected": "eyJhbGciOiJSUzI1NiJ9", + "header": { + "kid": "2010-12-29" + }, + "signature": "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw" + }, + { + "protected": "eyJhbGciOiJFUzI1NiJ9", + "header": { + "kid": "e9bc097a-ce51-4036-9562-d2ade882db0d" + }, + "signature": "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q" + } + ] + } + ], + "$defs": { + "signature": { + "type": "string", + "description": "Base 64 URL-encoded signature.", + "pattern": "^[A-Za-z0-9-_]{32,}$" + }, + "protected": { + "type": "string", + "description": "Base 64 URL-encoded protected header.", + "pattern": "^[A-Za-z0-9-_]{12,}$" + }, + "payload": { + "type": "string", + "description": "Base 64 URL-encoded data to be secured.", + "pattern": "^[A-Za-z0-9-_]{12,}$" + } + } +} diff --git a/schema/v2/payload.schema.json b/schema/v2/payload.schema.json new file mode 100644 index 0000000..71376ed --- /dev/null +++ b/schema/v2/payload.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pgxn.org/meta/v2/payload.schema.json", + "title": "PGXN Release Payload", + "description": "JSON Web Signature release payload populated by PGXN.", + "type": "object", + "properties": { + "user": { + "$ref": "term.schema.json", + "description": "The PGXN username for the user who released the distribution to PGXN.", + "examples": ["theory", "keithf4"] + }, + "date": { + "type": "string", + "format": "date-time", + "description": "The release timestamp.", + "examples": ["2024-09-12T19:56:49Z"] + }, + "uri": { + "type": "string", + "format": "uri-reference", + "pattern": "^dist/", + "description": "Path to the release file relative to a PGXN base URL.", + "examples": [ + "dist/pair/0.1.7/pair-0.1.7.zip", + "dist/plv8/3.2.3/plv8-3.2.3.zip" + ] + }, + "digests": { + "$ref": "digests.schema.json" + } + }, + "required": ["user", "date", "uri", "digests"], + "additionalProperties": false, + "examples": [ + { + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + } + }, + { + "user": "theory", + "date": "2024-09-13T17:32:55Z", + "uri": "dist/pair/0.1.7/pair-0.1.7.zip", + "digests": { + "sha256": "257b71aa57a28d62ddbb301333b3521ea3dc56f17551fa0e4516b03998abb089", + "sha512": "b353b5a82b3b54e95f4a2859e7a2bd0648abcb35a7c3612b126c2c75438fc2f8e8ee1f19e61f30fa54d7bb64bcf217ed1264722b497bcb613f82d78751515b67" + } + } + ] +} diff --git a/src/tests/v2.rs b/src/tests/v2.rs index 07a70a2..82b06bc 100644 --- a/src/tests/v2.rs +++ b/src/tests/v2.rs @@ -15,14 +15,12 @@ fn test_schema_v2() -> Result<(), Box> { #[test] fn test_v2_term() -> Result<(), Box> { - // Load the schemas and compile the term schema. let compiler = new_compiler("schema/v2")?; test_term_schema(compiler, SCHEMA_VERSION) } #[test] fn test_v2_tags() -> Result<(), Box> { - // Load the schemas and compile the tags schema. let compiler = new_compiler("schema/v2")?; test_tags_schema(compiler, SCHEMA_VERSION) } @@ -59,7 +57,7 @@ fn test_v2_semver() -> Result<(), Box> { #[test] fn test_v2_path() -> Result<(), Box> { - // Load the schemas and compile the semver schema. + // Load the schemas and compile the path schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "path"); @@ -103,7 +101,7 @@ fn test_v2_path() -> Result<(), Box> { #[test] fn test_v2_glob() -> Result<(), Box> { - // Load the schemas and compile the semver schema. + // Load the schemas and compile the glob schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "glob"); @@ -214,7 +212,7 @@ fn test_v2_version_range() -> Result<(), Box> { #[test] fn test_v2_license() -> Result<(), Box> { - // Load the schemas and compile the semver schema. + // Load the schemas and compile the license schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "license"); @@ -264,7 +262,7 @@ fn test_v2_license() -> Result<(), Box> { #[test] fn test_v2_purl() -> Result<(), Box> { - // Load the schemas and compile the semver schema. + // Load the schemas and compile the purl schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "purl"); @@ -308,7 +306,7 @@ fn test_v2_purl() -> Result<(), Box> { #[test] fn test_v2_platform() -> Result<(), Box> { - // Load the schemas and compile the semver schema. + // Load the schemas and compile the platform schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "platform"); @@ -394,7 +392,7 @@ fn test_v2_platform() -> Result<(), Box> { #[test] fn test_v2_platforms() -> Result<(), Box> { - // Load the schemas and compile the semver schema. + // Load the schemas and compile the platforms schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "platforms"); @@ -442,7 +440,7 @@ fn test_v2_platforms() -> Result<(), Box> { #[test] fn test_v2_maintainers() -> Result<(), Box> { - // Load the schemas and compile the maintainer schema. + // Load the schemas and compile the maintainers schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "maintainers"); @@ -757,7 +755,7 @@ fn test_v2_extension() -> Result<(), Box> { #[test] fn test_v2_module() -> Result<(), Box> { - // Load the schemas and compile the extension schema. + // Load the schemas and compile the module schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "module"); @@ -926,7 +924,7 @@ fn test_v2_module() -> Result<(), Box> { #[test] fn test_v2_app() -> Result<(), Box> { - // Load the schemas and compile the extension schema. + // Load the schemas and compile the app schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "app"); @@ -1040,7 +1038,7 @@ fn test_v2_app() -> Result<(), Box> { #[test] fn test_v2_contents() -> Result<(), Box> { - // Load the schemas and compile the extension schema. + // Load the schemas and compile the contents schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "contents"); @@ -1179,7 +1177,7 @@ fn test_v2_contents() -> Result<(), Box> { #[test] fn test_v2_meta_spec() -> Result<(), Box> { - // Load the schemas and compile the maintainer schema. + // Load the schemas and compile the meta-spec schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "meta-spec"); @@ -1233,7 +1231,7 @@ fn test_v2_meta_spec() -> Result<(), Box> { #[test] fn test_v2_categories() -> Result<(), Box> { - // Load the schemas and compile the maintainer schema. + // Load the schemas and compile the categories schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "categories"); @@ -1302,7 +1300,7 @@ fn test_v2_categories() -> Result<(), Box> { #[test] fn test_v2_classifications() -> Result<(), Box> { - // Load the schemas and compile the maintainer schema. + // Load the schemas and compile the classifications schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "classifications"); @@ -1373,7 +1371,7 @@ fn test_v2_classifications() -> Result<(), Box> { #[test] fn test_v2_ignore() -> Result<(), Box> { - // Load the schemas and compile the semver schema. + // Load the schemas and compile the ignore schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "ignore"); @@ -1431,7 +1429,7 @@ fn test_v2_ignore() -> Result<(), Box> { #[test] fn test_v2_phase() -> Result<(), Box> { - // Load the schemas and compile the maintainer schema. + // Load the schemas and compile the phase schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "phase"); @@ -1558,7 +1556,7 @@ fn test_v2_phase() -> Result<(), Box> { #[test] fn test_v2_packages() -> Result<(), Box> { - // Load the schemas and compile the maintainer schema. + // Load the schemas and compile the packages schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "packages"); @@ -1712,7 +1710,7 @@ fn test_v2_packages() -> Result<(), Box> { #[test] fn test_v2_postgres() -> Result<(), Box> { - // Load the schemas and compile the maintainer schema. + // Load the schemas and compile the postgres schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "postgres"); @@ -1772,7 +1770,7 @@ fn test_v2_postgres() -> Result<(), Box> { #[test] fn test_v2_pipeline() -> Result<(), Box> { - // Load the schemas and compile the semver schema. + // Load the schemas and compile the pipeline schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "pipeline"); @@ -1816,7 +1814,7 @@ fn test_v2_pipeline() -> Result<(), Box> { #[test] fn test_v2_dependencies() -> Result<(), Box> { - // Load the schemas and compile the maintainer schema. + // Load the schemas and compile the dependencies schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "dependencies"); @@ -2071,7 +2069,7 @@ fn test_v2_dependencies() -> Result<(), Box> { #[test] fn test_v2_variations() -> Result<(), Box> { - // Load the schemas and compile the maintainer schema. + // Load the schemas and compile the variations schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "variations"); @@ -2201,7 +2199,7 @@ fn test_v2_variations() -> Result<(), Box> { #[test] fn test_v2_badges() -> Result<(), Box> { - // Load the schemas and compile the maintainer schema. + // Load the schemas and compile the badges schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "badges"); @@ -2314,7 +2312,7 @@ fn test_v2_badges() -> Result<(), Box> { #[test] fn test_v2_resources() -> Result<(), Box> { - // Load the schemas and compile the maintainer schema. + // Load the schemas and compile the resources schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "resources"); @@ -2440,7 +2438,7 @@ fn test_v2_resources() -> Result<(), Box> { #[test] fn test_v2_artifacts() -> Result<(), Box> { - // Load the schemas and compile the maintainer schema. + // Load the schemas and compile the artifacts schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "artifacts"); @@ -3264,7 +3262,7 @@ fn test_v2_distribution() -> Result<(), Box> { #[test] fn test_v2_digests() -> Result<(), Box> { - // Load the schemas and compile the maintainer schema. + // Load the schemas and compile the digests schema. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let id = id_for(SCHEMA_VERSION, "digests"); @@ -4300,8 +4298,1373 @@ fn test_v2_pgxn_jws() -> Result<(), Box> { } #[test] -fn test_v2_release() -> Result<(), Box> { - // Load the schemas and compile the distribution schema. +fn test_v2_payload() -> Result<(), Box> { + // Load the schemas and compile the payload schema. + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "payload"); + let idx = compiler.compile(&id, &mut schemas)?; + + for (name, json) in [ + ( + "general", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + } + }), + ), + ( + "multi general", + json!({ + "user": "theory", + "date": "2024-09-13T17:32:55Z", + "uri": "dist/pair/0.1.7/pair-0.1.7.zip", + "digests": { + "sha256": "257b71aa57a28d62ddbb301333b3521ea3dc56f17551fa0e4516b03998abb089", + "sha512": "b353b5a82b3b54e95f4a2859e7a2bd0648abcb35a7c3612b126c2c75438fc2f8e8ee1f19e61f30fa54d7bb64bcf217ed1264722b497bcb613f82d78751515b67" + } + }), + ), + ] { + if let Err(e) = schemas.validate(&json, idx) { + panic!("{name} failed: {e}"); + } + } + + // let payload = json!({ + // "user": "theory", + // "date": "2024-07-20T20:34:34Z", + // "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + // "digests": { + // "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + // } + // }); + + for (name, json) in [ + ("no data", json!({})), + ("array", json!([])), + ("empty object", json!({})), + ("string", json!("2.0.0")), + ("empty string", json!("")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + // user + ( + "missing user", + json!({ + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "user empty", + json!({ + "user": "", + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "user number", + json!({ + "user": 42, + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "user null", + json!({ + "user": null, + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "user bool", + json!({ + "user": true, + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "user array", + json!({ + "user": ["theory"], + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "user object", + json!({ + "user": {"x": "hi"}, + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "user too short", + json!({ + "user": "x", + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + // date + ( + "missing date", + json!({ + "user": "theory", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "date null", + json!({ + "user": "theory", + "date": null, + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "date bool", + json!({ + "user": "theory", + "date": true, + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "date number", + json!({ + "user": "theory", + "date": 42, + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "date array", + json!({ + "user": "theory", + "date": ["2024-07-20T20:34:34Z"], + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "date object", + json!({ + "user": "theory", + "date": {"x": "2024-07-20T20:34:34Z"}, + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "invalid date", + json!({ + "user": "theory", + "date": ["2024-07-20T27:34:34Z"], + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + // url + ( + "missing uri", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "uri null", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": null, + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "uri number", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": 42, + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "uri bool", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": true, + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "uri empty", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": "", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "uri array", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": ["/dist/semver/0.40.0/semver-0.40.0.zip"], + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "uri object", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": {"x": "/dist/semver/0.40.0/semver-0.40.0.zip"}, + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "invalid uri", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": "not a URI", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + ( + "bad uri start", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": "/dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { + "sha1": "fe8c013f991b5f537c39fb0c0b04bc955457675a" + }, + }), + ), + // digests + ( + "missing digests", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip" + }), + ), + ( + "empty digests", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": {}, + }), + ), + ( + "invalid digest", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": { "sha1": "nope" }, + }), + ), + ( + "null digests", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": null, + }), + ), + ( + "number digests", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": 42, + }), + ), + ( + "string digests", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": "fe8c013f991b5f537c39fb0c0b04bc955457675a", + }), + ), + ( + "array digests", + json!({ + "user": "theory", + "date": "2024-07-20T20:34:34Z", + "uri": "dist/semver/0.40.0/semver-0.40.0.zip", + "digests": ["fe8c013f991b5f537c39fb0c0b04bc955457675a"], + }), + ), + ] { + if schemas.validate(&json, idx).is_ok() { + panic!("{name} unexpectedly passed!") + } + } + + Ok(()) +} + +#[test] +fn test_v2_jwk() -> Result<(), Box> { + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "jwk"); + let idx = compiler.compile(&id, &mut schemas)?; + + for (name, json) in [ + ("kty EC", json!({"kty": "EC"})), + ("kty RSA", json!({"kty": "RSA"})), + ("kty empty", json!({"kty": ""})), + ("use enc", json!({"kty": "EC", "use": "enc"})), + ("use sig", json!({"kty": "EC", "use": "sig"})), + ("key_ops", json!({"kty": "EC", "key_ops": ["read"]})), + ( + "key_ops 2", + json!({"kty": "EC", "key_ops": ["read", "write"]}), + ), + ("alg", json!({"kty": "EC", "alg": "HS256"})), + ("kid", json!({"kty": "EC", "kid": "hi"})), + ("x5u", json!({"kty": "EC", "x5u": "https://example.com"})), + ( + "x5c", + json!({"kty": "EC", "x5c": ["VGhpcyBpcyBhIHRlc3Q=", "q0V4Ot8L8YlUzZm2BytfHTK0KQLzCyqZrdSpnyAci3E="]}), + ), + ( + "x5c 2", + json!({"kty": "EC", "x5c": ["VGhpcyBpcyBhIHRlc3Q="]}), + ), + ("x5t", json!({"kty": "EC", "x5t": "012345678912"})), + ( + "x5t#S256", + json!({"kty": "EC", "x5t#S256": "abgU7GuNO8EfzYDFmryoploCskBljphPWnpJ0po"}), + ), + ("other string prop", json!({"kty": "EC", "go": "whatever"})), + ("other bool prop", json!({"kty": "EC", "safe": true})), + ("custom x_ prop", json!({"kty": "EC", "x_": true})), + ("custom X_ prop", json!({"kty": "EC", "X_": true})), + ( + "everything", + json!({ + "kty": "EC", + "use": "sig", + "key_ops": ["read"], + "alg": "HS256", + "kid": "42", + "x5u": "https://example.com", + "x5c": ["VGhpcyBpcyBhIHRlc3Q=", "q0V4Ot8L8YlUzZm2BytfHTK0KQLzCyqZrdSpnyAci3E="], + "x5t": "012345678912", + "x5t#S256": "abgU7GuNO8EfzYDFmryoploCskBljphPWnpJ0po", + }), + ), + ] { + if let Err(e) = schemas.validate(&json, idx) { + panic!("{name} failed: {e}"); + } + } + + for (name, json) in [ + ("array", json!(["hi"])), + ("string", json!("hi")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("number", json!(42)), + ("empty object", json!({})), + // kty + ("kty array", json!({"kty": ["HS256"]})), + ("kty object", json!({"kty": {"HS256": "HS256"}})), + ("kty true", json!({"kty": true})), + ("kty null", json!({"kty": null})), + ("kty number", json!({"kty": 42})), + ("no kty", json!({"alg": "HS256"})), + // use + ("use array", json!({"kty": "EC", "use": ["enc"]})), + ("use object", json!({"kty": "EC", "use": {"ecn": true}})), + ("use true", json!({"kty": "EC", "use": true})), + ("use null", json!({"kty": "EC", "use": null})), + ("use number", json!({"kty": "EC", "use": 42})), + // key_ops + ("key_ops empty array", json!({"kty": "EC", "key_ops": []})), + ( + "key_ops object", + json!({"kty": "EC", "key_ops": {"read": true}}), + ), + ("key_ops true", json!({"kty": "EC", "key_ops": true})), + ("key_ops null", json!({"kty": "EC", "key_ops": null})), + ("key_ops number", json!({"kty": "EC", "key_ops": 42})), + ("key_ops true item", json!({"kty": "EC", "key_ops": [true]})), + ("key_ops null item", json!({"kty": "EC", "key_ops": [null]})), + ("key_ops number item", json!({"kty": "EC", "key_ops": [42]})), + ( + "key_ops array item", + json!({"kty": "EC", "key_ops": [["read"]]}), + ), + ( + "key_ops object item", + json!({"kty": "EC", "key_ops": [{"read": true}]}), + ), + // alg + ("alg array", json!({"kty": "EC", "alg": ["enc"]})), + ("alg object", json!({"kty": "EC", "alg": {"ecn": true}})), + ("alg true", json!({"kty": "EC", "alg": true})), + ("alg null", json!({"kty": "EC", "alg": null})), + ("alg number", json!({"kty": "EC", "alg": 42})), + // kid + ("kid array", json!({"kty": "EC", "kid": ["enc"]})), + ("kid object", json!({"kty": "EC", "kid": {"ecn": true}})), + ("kid true", json!({"kty": "EC", "kid": true})), + ("kid null", json!({"kty": "EC", "kid": null})), + ("kid number", json!({"kty": "EC", "kid": 42})), + // x5u + ("x5u array", json!({"kty": "EC", "x5u": ["HS256"]})), + ( + "x5u object", + json!({"kty": "EC", "x5u": {"HS256": "HS256"}}), + ), + ("x5u non-uri", json!({"kty": "EC", "x5u": "not a uri"})), + ("x5u true", json!({"kty": "EC", "x5u": true})), + ("x5u null", json!({"kty": "EC", "x5u": null})), + ("x5u number", json!({"kty": "EC", "x5u": 42})), + // x5c + ("x5c empty array", json!({"kty": "EC", "x5c": []})), + ("x5c object", json!({"kty": "EC", "x5c": {"read": true}})), + ("x5c true", json!({"kty": "EC", "x5c": true})), + ("x5c null", json!({"kty": "EC", "x5c": null})), + ("x5c number", json!({"kty": "EC", "x5c": 42})), + ("x5c true item", json!({"kty": "EC", "x5c": [true]})), + ("x5c null item", json!({"kty": "EC", "x5c": [null]})), + ("x5c number item", json!({"kty": "EC", "x5c": [42]})), + ("x5c array item", json!({"kty": "EC", "x5c": [["read"]]})), + ( + "x5c object item", + json!({"kty": "EC", "x5c": [{"read": true}]}), + ), + ( + "x5c not base64", + json!({"kty": "EC", "x5c": ["not base64"]}), + ), + ( + "x5c base64 URL", + json!({"kty": "EC", "x5c": ["DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-"]}), + ), + // x5t + ("x5t array", json!({"kty": "EC", "x5t": ["enc"]})), + ("x5t object", json!({"kty": "EC", "x5t": {"ecn": true}})), + ("x5t true", json!({"kty": "EC", "x5t": true})), + ("x5t null", json!({"kty": "EC", "x5t": null})), + ("x5t number", json!({"kty": "EC", "x5t": 42})), + ( + "x5t not base64 URL", + json!({"kty": "EC", "x5t": "not base64"}), + ), + // x5t#S256 + ("x5t#S256 array", json!({"kty": "EC", "x5t#S256": ["enc"]})), + ( + "x5t#S256 object", + json!({"kty": "EC", "x5t#S256": {"ecn": true}}), + ), + ("x5t#S256 true", json!({"kty": "EC", "x5t#S256": true})), + ("x5t#S256 null", json!({"kty": "EC", "x5t#S256": null})), + ("x5t#S256 number", json!({"kty": "EC", "x5t#S256": 42})), + ( + "x5t#S256 not base64 URL", + json!({"kty": "EC", "x5t#S256": "not base64"}), + ), + ] { + if schemas.validate(&json, idx).is_ok() { + panic!("{name} unexpectedly passed!") + } + } + Ok(()) +} + +#[test] +fn test_v2_jws_header() -> Result<(), Box> { + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "jws-header"); + let idx = compiler.compile(&id, &mut schemas)?; + + for (name, json) in [ + ("alg", json!({"alg": "HS256"})), + ("empty alg", json!({"alg": ""})), + ("jku", json!({"jku": "https://example.com"})), + ("jwk kty", json!({"jwk": {"kty": "EC"}})), + ("jwk kty use", json!({"jwk": {"kty": "EC", "use": "sig"}})), + ("kid", json!({"kid": "big kid"})), + ("empty kid", json!({"kid": ""})), + ("x5u", json!({"x5u": "x:y"})), + ("x5c", json!({"x5c": ["VGhpcyBpcyBhIHRlc3Q="]})), + ( + "x5c 2", + json!({"x5c": ["VGhpcyBpcyBhIHRlc3Q=", "y4MKFQUlW9XrfFXCmZeYXUZkqpc="]}), + ), + ("x5t", json!({"x5t": "012345678912"})), + ( + "x5t#S256", + json!({"x5t#S256": "abgU7GuNO8EfzYDFmryoploCskBljphPWnpJ0po"}), + ), + ("typ", json!({"typ": "extension"})), + ("empty typ", json!({"typ": ""})), + ("cty", json!({"cty": "text/plain"})), + ("empty cty", json!({"cty": ""})), + ( + "everything", + json!({ + "alg": "HS256", + "jku": "https://example.com", + "jwk": { + "kty": "EC", + "use": "sig", + "key_ops": ["read"], + "alg": "HS256", + "kid": "99", + "x5u": "https://example.com", + "x5c": ["VGhpcyBpcyBhIHRlc3Q="], + "x5t": "012345678912", + "x5t#S256": "abgU7GuNO8EfzYDFmryoploCskBljphPWnpJ0po", + }, + "kid": "2024-07-01", + "x5u": "https://example.com", + "x5c": ["VGhpcyBpcyBhIHRlc3Q="], + "x5t": "012345678912", + "x5t#S256": "abgU7GuNO8EfzYDFmryoploCskBljphPWnpJ0po", + "typ": "extension", + "cty": "text/plain", + }), + ), + ] { + if let Err(e) = schemas.validate(&json, idx) { + panic!("{name} failed: {e}"); + } + } + + for (name, json) in [ + ("array", json!(["hi"])), + ("string", json!("hi")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("number", json!(42)), + ("empty object", json!({})), + // alg + ("alg array", json!({"alg": ["HS256"]})), + ("alg object", json!({"alg": {"HS256": "HS256"}})), + ("alg true", json!({"alg": true})), + ("alg null", json!({"alg": null})), + ("alg number", json!({"alg": 42})), + // jku + ("jku array", json!({"jku": ["HS256"]})), + ("jku object", json!({"jku": {"HS256": "HS256"}})), + ("jku non-uri", json!({"jku": "not a uri"})), + ("jku true", json!({"jku": true})), + ("jku null", json!({"jku": null})), + ("jku number", json!({"jku": 42})), + // jwk + ("jwk array", json!({"jwk": ["HS256"]})), + ("jwk string", json!({"jwk": "hi"})), + ("jwk true", json!({"jwk": true})), + ("jwk null", json!({"jwk": null})), + ("jwk number", json!({"jwk": 42})), + ("jwk empty object", json!({"jwk": {}})), + ("jwk no kty", json!({"jwk": {"alg": "HS256"}})), + // kid + ("kid array", json!({"kid": ["HS256"]})), + ("kid object", json!({"kid": {"HS256": "HS256"}})), + ("kid true", json!({"kid": true})), + ("kid null", json!({"kid": null})), + ("kid number", json!({"kid": 42})), + // x5u + ("x5u array", json!({"x5u": ["HS256"]})), + ("x5u object", json!({"x5u": {"HS256": "HS256"}})), + ("x5u non-uri", json!({"x5u": "not a uri"})), + ("x5u true", json!({"x5u": true})), + ("x5u null", json!({"x5u": null})), + ("x5u number", json!({"x5u": 42})), + // x5c + ("x5c empty array", json!({"x5c": []})), + ("x5c object", json!({"x5c": {"read": true}})), + ("x5c true", json!({"x5c": true})), + ("x5c null", json!({"x5c": null})), + ("x5c number", json!({"x5c": 42})), + ("x5c true item", json!({"x5c": [true]})), + ("x5c null item", json!({"x5c": [null]})), + ("x5c number item", json!({"x5c": [42]})), + ("x5c array item", json!({"x5c": [["read"]]})), + ("x5c object item", json!({"x5c": [{"read": true}]})), + ("x5c not base64", json!({"x5c": ["not base64"]})), + ( + "x5c base64 URL", + json!({"x5c": ["DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-"]}), + ), + // x5t + ("x5t array", json!({"x5t": ["enc"]})), + ("x5t object", json!({"x5t": {"ecn": true}})), + ("x5t true", json!({"x5t": true})), + ("x5t null", json!({"x5t": null})), + ("x5t number", json!({"x5t": 42})), + ("x5t not base64 URL", json!({"x5t": "not base64"})), + // x5t#S256 + ("x5t#S256 array", json!({"x5t#S256": ["enc"]})), + ("x5t#S256 object", json!({"x5t#S256": {"ecn": true}})), + ("x5t#S256 true", json!({"x5t#S256": true})), + ("x5t#S256 null", json!({"x5t#S256": null})), + ("x5t#S256 number", json!({"x5t#S256": 42})), + ("x5t#S256 not base64 URL", json!({"x5t#S256": "not base64"})), + // typ + ("typ array", json!({"typ": ["extension"]})), + ("typ object", json!({"typ": {"extension": "extension"}})), + ("typ true", json!({"typ": true})), + ("typ null", json!({"typ": null})), + ("typ number", json!({"typ": 42})), + // cty + ("cty array", json!({"cty": ["text/plain"]})), + ("cty object", json!({"cty": {"type": "text/plain"}})), + ("cty true", json!({"cty": true})), + ("cty null", json!({"cty": null})), + ("cty number", json!({"cty": 42})), + ] { + if schemas.validate(&json, idx).is_ok() { + panic!("{name} unexpectedly passed!") + } + } + Ok(()) +} + +#[test] +fn test_v2_jws() -> Result<(), Box> { + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "jws"); + let idx = compiler.compile(&id, &mut schemas)?; + + for (name, json) in [ + ( + "general", + json!({ + "payload": "abcdefghijkl", + "signatures": [ + {"signature": "abcdefghijklmnopqrstuvwxyz012345"} + ] + }), + ), + ( + "flattened", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + }), + ), + ( + "full general signature", + json!({ + "payload": "abcdefghijkl", + "signatures": [ + { + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": "012345678912", + "header": { "kid": "42" }, + } + ] + }), + ), + ( + "full flattened signature", + json!({ + "payload": "abcdefghijkl", + "protected": "012345678912", + "header": { "kid": "42" }, + "signature": "abcdefghijklmnopqrstuvwxyz012345", + }), + ), + ( + "multiple signatures", + json!({ + "payload": "abcdefghijkl", + "signatures": [ + { + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": "012345678912", + "header": { "kid": "42" }, + }, + {"signature": "098765432109876543210987654321209"} + ] + }), + ), + ( + "general plus additional properties", + json!({ + "payload": "abcdefghijkl", + "hello": "yourself", + "signatures": [ + {"signature": "abcdefghijklmnopqrstuvwxyz012345"} + ] + }), + ), + ( + "flattened plus additional properties", + json!({ + "payload": "abcdefghijkl", + "safe": true, + "clamp": "u", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + }), + ), + ] { + if let Err(e) = schemas.validate(&json, idx) { + panic!("{name} failed: {e}"); + } + } + + for (name, json) in [ + ("array", json!(["hi"])), + ("string", json!("hi")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("number", json!(42)), + ("empty object", json!({})), + ( + "no flattened payload", + json!({"signature": "abcdefghijklmnopqrstuvwxyz012345"}), + ), + ( + "no general payload", + json!({"signatures": [{"signature": "abcdefghijklmnopqrstuvwxyz012345"}]}), + ), + ( + "no signatures or signature", + json!({"payload": "abcdefghijkl"}), + ), + // payload + ("true payload", json!({"payload": true})), + ("false payload", json!({"payload": false})), + ("null payload", json!({"payload": null})), + ("number payload", json!({"payload": 42})), + ("array payload", json!({"payload": ["hi"]})), + ("object payload", json!({"payload": {}})), + ("short payload", json!({"payload": "012345678901"})), + ("invalid payload", json!({"payload": "not base64 url"})), + // signatures + ( + "true signatures", + json!({"payload": "abcdefghijkl", "signatures": true}), + ), + ( + "false signatures", + json!({"payload": "abcdefghijkl", "signatures": false}), + ), + ( + "null signatures", + json!({"payload": "abcdefghijkl", "signatures": null}), + ), + ( + "string signatures", + json!({ + "payload": "abcdefghijkl", + "signatures": "abcdefghijklmnopqrstuvwxyz012345", + }), + ), + ( + "number signatures", + json!({"payload": "abcdefghijkl", "signatures": 42}), + ), + ( + "object signatures", + json!({"payload": "abcdefghijkl", "signatures": { + "signature": "abcdefghijklmnopqrstuvwxyz012345", + }}), + ), + ( + "empty signatures", + json!({"payload": "abcdefghijkl", "signatures": []}), + ), + // signatures items + ( + "signatures string item", + json!({ + "payload": "abcdefghijkl", + "signatures": ["abcdefghijklmnopqrstuvwxyz012345"], + }), + ), + ( + "signatures bool item", + json!({"payload": "abcdefghijkl", "signatures": [true]}), + ), + ( + "signatures null item", + json!({"payload": "abcdefghijkl", "signatures": [null]}), + ), + ( + "signatures number item", + json!({"payload": "abcdefghijkl", "signatures": [42]}), + ), + ( + "signatures array item", + json!({ + "payload": "abcdefghijkl", + "signatures": [["abcdefghijklmnopqrstuvwxyz012345"]], + }), + ), + ( + "signatures empty item", + json!({"payload": "abcdefghijkl", "signatures": [{}]}), + ), + // signatures signature + ( + "signatures bool signature", + json!({ + "payload": "abcdefghijkl", + "signatures": [{"signature": true}], + }), + ), + ( + "signatures null signature", + json!({ + "payload": "abcdefghijkl", + "signatures": [{"signature": null}], + }), + ), + ( + "signatures number signature", + json!({ + "payload": "abcdefghijkl", + "signatures": [{"signature": 42}], + }), + ), + ( + "signatures array signature", + json!({ + "payload": "abcdefghijkl", + "signatures": [{"signature": ["abcdefghijklmnopqrstuvwxyz012345"]}], + }), + ), + ( + "signatures object signature", + json!({ + "payload": "abcdefghijkl", + "signatures": [{"signature": {}}], + }), + ), + ( + "signatures empty string signature", + json!({ + "payload": "abcdefghijkl", + "signatures": [{"signature": ""}], + }), + ), + ( + "signatures short signature", + json!({ + "payload": "abcdefghijkl", + "signatures": [{"signature": "abcdefghijklmnopqrstuvwxyz01234"}], + }), + ), + ( + "signatures signature not base64", + json!({ + "payload": "abcdefghijkl", + "signatures": [{"signature": "abcdefghijklmnopqrstuvwxyz01234#"}], + }), + ), + // signatures header + ( + "signatures header empty object", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "header": {}, + }], + }), + ), + ( + "signatures header array", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "header": [], + }], + }), + ), + ( + "signatures header bool", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "header": true, + }], + }), + ), + ( + "signatures header null", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "header": null, + }], + }), + ), + ( + "signatures header number", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "header": 42, + }], + }), + ), + ( + "signatures header invalid", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "header": {"jku": "not a uri"}, + }], + }), + ), + // signatures protected + ( + "signatures protected bool", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": true, + }], + }), + ), + ( + "signatures protected null", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": null, + }], + }), + ), + ( + "signatures protected number", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": 42, + }], + }), + ), + ( + "signatures protected array", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": ["012345678912"], + }], + }), + ), + ( + "signatures protected object", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": {"012345678912": true}, + }], + }), + ), + ( + "signatures protected empty string", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": "", + }], + }), + ), + ( + "signatures protected too short", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": "01234567891", + }], + }), + ), + ( + "signatures protected not base64", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": "this is not base 64", + }], + }), + ), + ( + "signatures protected not base64 URL", + json!({ + "payload": "abcdefghijkl", + "signatures": [{ + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": "012345678912+", + }], + }), + ), + // signature + ( + "true signature", + json!({"payload": "abcdefghijkl", "signature": true}), + ), + ( + "false signature", + json!({"payload": "abcdefghijkl", "signature": false}), + ), + ( + "null signature", + json!({"payload": "abcdefghijkl", "signature": null}), + ), + ( + "number signature", + json!({"payload": "abcdefghijkl", "signature": 42}), + ), + ( + "array signature", + json!({ + "payload": "abcdefghijkl", + "signature": ["abcdefghijklmnopqrstuvwxyz012345"], + }), + ), + ( + "object signature", + json!({"payload": "abcdefghijkl", "signature": { + "signature": "abcdefghijklmnopqrstuvwxyz012345", + }}), + ), + ( + "empty signature", + json!({"payload": "abcdefghijkl", "signature": ""}), + ), + ( + "short signature", + json!({"payload": "abcdefghijkl", "signature": "abcdefghijklmnopqrstuvwxyz01234"}), + ), + ( + "not base 64 url signature", + json!({"payload": "abcdefghijkl", "signature": "abcdefghijklmnopqrstuvwxyz012345+"}), + ), + // header + ( + "header empty object", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "header": {}, + }), + ), + ( + "header array", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "header": [], + }), + ), + ( + "header bool", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "header": true, + }), + ), + ( + "header null", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "header": null, + }), + ), + ( + "header number", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "header": 42, + }), + ), + ( + "header invalid", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "header": {"jku": "not a uri"}, + }), + ), + // protected + ( + "protected bool", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": true, + }), + ), + ( + "protected null", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": null, + }), + ), + ( + "protected number", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": 42, + }), + ), + ( + "protected array", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": ["012345678912"], + }), + ), + ( + "protected object", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": {"012345678912": true}, + }), + ), + ( + "protected empty string", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": "", + }), + ), + ( + "protected too short", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": "01234567891", + }), + ), + ( + "protected not base64", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": "this is not base 64", + }), + ), + ( + "protected not base64 URL", + json!({ + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + "protected": "012345678912+", + }), + ), + ] { + if schemas.validate(&json, idx).is_ok() { + panic!("{name} unexpectedly passed!") + } + } + + Ok(()) +} + +#[test] +fn test_v2_certs() -> Result<(), Box> { + let mut compiler = new_compiler("schema/v2")?; + let mut schemas = Schemas::new(); + let id = id_for(SCHEMA_VERSION, "certs"); + let idx = compiler.compile(&id, &mut schemas)?; + + for (name, json) in [ + ( + "pgxn flattened", + json!({"pgxn": { + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + }}), + ), + ( + "pgxn general", + json!({"pgxn": { + "payload": "abcdefghijkl", + "signatures": [ + {"signature": "abcdefghijklmnopqrstuvwxyz012345"}, + ], + }}), + ), + ( + "pgxn plus additional property x_", + json!({ + "pgxn": { + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + }, + "x_abc": true, + }), + ), + ( + "pgxn plus additional property X_", + json!({ + "pgxn": { + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + }, + "X_yz": {"kid": "anna"}, + }), + ), + ] { + if let Err(e) = schemas.validate(&json, idx) { + panic!("{name} failed: {e}"); + } + } + + for (name, json) in [ + ("array", json!(["hi"])), + ("string", json!("hi")), + ("true", json!(true)), + ("false", json!(false)), + ("null", json!(null)), + ("number", json!(42)), + ("empty object", json!({})), + ( + "invalid property", + json!({ + "pgxn": { + "payload": "abcdefghijkl", + "signature": "abcdefghijklmnopqrstuvwxyz012345", + }, + "hi": "there", + }), + ), + ("invalid pgxn", json!({"pgxn": {"payload": "abcdefghijkl"}})), + ("pgxn array", json!({"pgxn": ["hi"]})), + ("pgxn string", json!({"pgxn": "pgxn hi"})), + ("pgxn true", json!({"pgxn": true})), + ("pgxn false", json!({"pgxn": false})), + ("pgxn null", json!({"pgxn": null})), + ("pgxn number", json!({"pgxn": 42})), + ("pgxn empty object", json!({"pgxn": {}})), + ] { + if schemas.validate(&json, idx).is_ok() { + panic!("{name} unexpectedly passed!") + } + } + + Ok(()) +} + +#[test] +fn test_v2_release() -> Result<(), Box> { + // Load the schemas and compile the release and distribution schemas. let mut compiler = new_compiler("schema/v2")?; let mut schemas = Schemas::new(); let release_id = id_for(SCHEMA_VERSION, "release");