diff --git a/Cargo.lock b/Cargo.lock index 492b455..72394f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,6 +54,14 @@ dependencies = [ "vstd", ] +[[package]] +name = "clazel-der" +version = "0.0.0" +dependencies = [ + "der", + "vstd", +] + [[package]] name = "clazel-dns" version = "0.0.0" @@ -210,6 +218,12 @@ dependencies = [ "vstd", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "convert_case" version = "0.4.0" @@ -241,6 +255,16 @@ dependencies = [ "url", ] +[[package]] +name = "der" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "document-features" version = "0.2.12" @@ -661,6 +685,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + [[package]] name = "zmij" version = "1.0.23" diff --git a/crates/clazel-der/Cargo.toml b/crates/clazel-der/Cargo.toml new file mode 100644 index 0000000..64566f3 --- /dev/null +++ b/crates/clazel-der/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "clazel-der" +version = "0.0.0" +edition.workspace = true +license.workspace = true +publish.workspace = true +description = "ASN.1 DER (ITU-T X.690): a cursor-style decoder and an encoder for the types X.509 uses." + +[dependencies] +vstd = "=0.0.0-2026-10-04-0306" + +# Test-only reference engine for the differential test in +# tests/conformance_der.rs (class `test` in docs/DEPENDENCIES.md: never +# shipped). Default features are off; `alloc` gives the owned types and +# `to_der`, `oid` the OBJECT IDENTIFIER type, so its only dependencies are `zeroize` and +# `const-oid`. +[dev-dependencies] +der = { version = "=0.8.2", default-features = false, features = ["alloc", "oid"] } + +# Verification tier (docs/VERIFICATION.md). `tier` is what is proven about +# the shipped code today; `target-tier` is what the policy requires. +[package.metadata.clazel] +tier = 0 +target-tier = 2 + +[package.metadata.verus] +verify = true + +[lints] +workspace = true diff --git a/crates/clazel-der/README.md b/crates/clazel-der/README.md new file mode 100644 index 0000000..279dab1 --- /dev/null +++ b/crates/clazel-der/README.md @@ -0,0 +1,147 @@ +# clazel-der + +ASN.1 DER (the Distinguished Encoding Rules of ITU-T X.690): a cursor-style +decoder and an encoder for the types X.509 certificates and PKCS#1 signatures +use, formally verified with Verus. Part of [Clazel](../../README.md), a web +browser whose parsers of untrusted input carry machine-checked proofs. It is +the base of items 3.5b (X.509 and path validation) and 3.5c (RSA PKCS#1 v1.5). + +## What it is + +- **Decoder.** Every reader takes the input, a position and an `end` + (`end <= input.len()`), reads the one value at the position and returns + where it stopped: `read_tlv` (the tag and the contents range), then typed + readers for BOOLEAN, INTEGER (its sign and magnitude octets), BIT STRING + (its unused-bits count and octets), OCTET STRING, NULL, OBJECT IDENTIFIER + (its arcs as `u64`), UTF8String, PrintableString, IA5String, UTCTime and + GeneralizedTime (their octets: the character and date rules are X.509's), + `expect_sequence` and `expect_set` (the inner range), `read_optional_context` + and `read_optional_explicit` for context-specific components (implicit and + explicit, §8.14), and `expect_end`. To descend into a constructed value, call + the readers again on `[cstart, cend)`; to require the end of contents, call + `expect_end`. Nothing is copied or reserved for a declared length: readers + return ranges, and `read_oid` pushes only arcs it has read. +- **Encoder.** `encode_tlv`, `encode_length` (the shortest form), + `encode_integer_unsigned`, `encode_oid`, `encode_sequence`, `encode_set`, + `encode_octet_string`, `encode_null`, `encode_boolean`, `encode_bit_string`, + `encode_octets` for the string types, `encode_context`, and + `encode_digest_info` (`SEQUENCE { SEQUENCE { OID, NULL }, OCTET STRING }`, + RFC 8017 §9.2). +- **Strict DER.** The decoder accepts only the DER encoding: definite length + in the fewest octets (§10.1), BOOLEAN `00` or `FF` (§11.1), INTEGER with no + redundant leading octet (§8.3.2), BIT STRING with 0 to 7 unused bits, all + zero (§8.6.2, §11.2.1), strings in the primitive form (§10.2), OBJECT + IDENTIFIER subidentifiers without `80` padding (§8.19.2), high tag numbers + only from 31 and without a leading zero digit (§8.1.2.4). +- **Limits** (stated at the top of `src/spec.rs`, each only rejects what the + standard accepts): tag numbers of 2^14 and above, long-form lengths of more + than 8 octets, and subidentifiers of 2^64 and above. Not checked here, and + left to X.509 (item 3.5b): the characters of the string types, the + UTCTime and GeneralizedTime formats, the order of SET OF components + (§11.6) and the omission of DEFAULT values (§11.5). + +Out of scope: BER (indefinite lengths, constructed strings, non-minimal +lengths) is rejected, not decoded, and there is no schema or `derive` layer: +callers compose the readers. + +## Verification + +Currently tier 0, target tier 2; promotion requires CI's full verification +on a pull request. See the [status table](../../README.md#verification-status) +for current numbers. The executable code has Verus proofs against an +executable specification transcribed from X.690, including absence of panics, +overflow and out-of-bounds indexing. + +- **Proven:** every reader and encoder meets its contract + (`src/contract.rs`). About the spec itself, with lemmas: + - *No over-read* (`lemmas_overread.rs`): every reader's result depends only + on the bytes of the value it read, `[pos, cend)`; changing any byte after + them, or at or after `end`, changes nothing. + - *Round trip and canonicity* (`lemmas_tlv.rs`, `lemmas_typed.rs`, + `lemmas_oid.rs`): reading what the encoder wrote gives back the value, and + encoding what a reader read gives back the original bytes, for the + identifier, the length, a whole value, and BOOLEAN, INTEGER (the + canonicity lemma covers the non-negative integers; a negative one is + proven only to be negative, and its minimality is proven through the + independent transcription), BIT STRING, OCTET STRING and the string + types, NULL, OBJECT IDENTIFIER, SEQUENCE, SET and context-specific tags. + So no two byte strings are accepted as the same value. + - *Shortest length:* `encode_length` is one octet below 128 and otherwise + the fewest octets, and `read_length` accepts exactly its output + (`lemma_encode_length_shortest`, `lemma_read_length_encode_length`, + `lemma_encode_length_read_length`). + - *Bounded reservation:* `read_oid` returns at most as many arcs as there + are contents octets plus one, and an encoder's output is at most 12 + octets longer than the contents it was given, so a declared length of + 2^64 reserves nothing. +- **Independent transcription:** `src/independent.rs` is written from + Kaliski, "A Layman's Guide to a Subset of ASN.1, BER, and DER" (RSA + Laboratories, 1993), sections 2 to 5, a different author's text of the same + rules, and is proven equal to the spec: the identifier, length and element + framing on every input, INTEGER, BIT STRING and NULL contents on every + input (the Guide's rule that the contents are the minimal two's complement + spelling of their value, against X.690's rule on the first two octets), + OBJECT IDENTIFIER contents whose first octet is below 128 (where the Guide's + "first octet is 40 * value1 + value2" applies), the value of an INTEGER, and + the typed readers for those types and for SEQUENCE and SET. The Guide has + no BOOLEAN, UTF8String or GeneralizedTime, so those have no second + transcription. The same agent wrote both transcriptions, so this is weaker + evidence than a second author's. +- **Examples:** `src/examples.rs` evaluates, in Verus, the examples of + X.690 §8.1.3, §8.2, §8.6, §8.8, §8.9, §8.14 and §8.19, Kaliski's examples + (including his X.501 `Name`) and the `DigestInfo` prefix of RFC 8017, in both + directions, with the non-DER forms printed beside them rejected. +- **Mutations:** every deliberately broken spec in `mutations.json`, one or + more per rule, is rejected by a certificate Verus checks. The tag numbers of + BOOLEAN, UTF8String and GeneralizedTime are not mutated: nothing but the + examples and the conformance test pins them (the Guide does not have them). +- **Conformance** (`tests/conformance_der.rs`): hand-built cases as Rust + literals (valid ones from X.690 and the Guide, and one for every rejection + rule: indefinite length, a long-form length that fits the short form, + leading zeros in a length, INTEGER `00 7F`, BOOLEAN `01`, BIT STRING with 8 + unused bits, an OID with `80` padding, a constructed OCTET STRING, truncated + contents, trailing bytes, and more); and a differential test against + RustCrypto's `der` 0.8.2 (default features off) on random valid values + (`der` writes and we read, we write and `der` reads, and the bytes are + equal) and on random and mutated bytes, where every disagreement must be in + a listed category. The `x509-limbo` corpus is item 3.5b's. +- **Modelling choices** (listed in `src/spec.rs`): a reader is a cursor over + `(input, pos, end)`; failure is `None`; the next position is the end of the + contents; INTEGERs are read as sign and magnitude range, OIDs as arcs. + +## Trust + +The proofs assume the Rust compiler, Verus and its solver, the `vstd` +library, and that the specs transcribe the standards correctly; the +conformance tests and the independent transcription are the check on that +last point, not a proof. The full list is in +[docs/VERIFICATION.md](../../docs/VERIFICATION.md). + +## Usage + +Not yet published to crates.io (`publish = false`). + +```rust +use clazel_der::decode::{expect_end, expect_sequence, read_integer, read_oid, read_tlv}; +use clazel_der::encode::{encode_oid, encode_sequence}; + +// SEQUENCE { OBJECT IDENTIFIER 1.2.840.113549, INTEGER 5 } +let mut body = encode_oid(&[1, 2, 840, 113549]); +body.extend_from_slice(&[0x02, 0x01, 0x05]); +let bytes = encode_sequence(&body); + +let (start, end) = expect_sequence(&bytes, 0, bytes.len()).expect("a SEQUENCE"); +let (arcs, next) = read_oid(&bytes, start, end).expect("an OID"); +assert_eq!(arcs, [1, 2, 840, 113549]); +let (negative, mag_start, mag_end, next) = read_integer(&bytes, next, end).expect("an INTEGER"); +assert!(!negative && bytes.get(mag_start..mag_end) == Some(&[5][..])); +assert!(expect_end(next, end)); + +// Every non-DER form is rejected: 38 is 0x26, never 0x81 0x26. +assert!(read_tlv(&[0x04, 0x81, 0x26], 0, 3).is_none()); +``` + +## Licence + +MIT, as in the repository's [LICENSE](../../LICENSE). See the +[main README](../../README.md) for the project. diff --git a/crates/clazel-der/mutations.json b/crates/clazel-der/mutations.json new file mode 100644 index 0000000..1268dd8 --- /dev/null +++ b/crates/clazel-der/mutations.json @@ -0,0 +1,771 @@ +{ + "mutations": [ + { + "id": "id-class-application", + "file": "src/spec.rs", + "find": " } else if bits == 1 {\n Class::Application", + "replace": " } else if bits == 1 {\n Class::Private", + "expect": "killed", + "reason": "Bits 01 are the application class (Table 1). Read as private, 41 decodes to a tag whose encoding is C1, so the bytes are not the encoding of what was read and lemma_encode_identifier_read_identifier is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_identifier_read_identifier(seq![0x41u8], 0, 1)", + "witness": "read_identifier(seq![0x41u8], 0, 1) == Some((Tag { class: Class::Private, constructed: false, number: 1 }, 1nat)) && !(seq![0x41u8].subrange(0, 1) =~= encode_identifier(Tag { class: Class::Private, constructed: false, number: 1 }))" + } + }, + { + "id": "id-constructed-bit", + "file": "src/spec.rs", + "find": "let constructed = (o / 32) % 2 == 1;", + "replace": "let constructed = (o / 32) % 2 == 0;", + "expect": "killed", + "reason": "Bit 6 set means constructed (8.1.2.5). Inverted, 30 (SEQUENCE) reads as primitive and re-encodes as 10, so the canonicity lemma is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_identifier_read_identifier(seq![0x30u8], 0, 1)", + "witness": "read_identifier(seq![0x30u8], 0, 1) == Some((Tag { class: Class::Universal, constructed: false, number: 16 }, 1nat)) && !(seq![0x30u8].subrange(0, 1) =~= encode_identifier(Tag { class: Class::Universal, constructed: false, number: 16 }))" + } + }, + { + "id": "id-low-form-limit", + "file": "src/spec.rs", + "find": " if o % 32 != 31 {", + "replace": " if o % 32 != 30 {", + "expect": "killed", + "reason": "Bits 5 to 1 all ones announce the high-tag-number form (8.1.2.4.1 c). Shifted by one, 1F reads as the low-form tag 31, whose encoding is 1F 1F: lemma_encode_identifier_read_identifier is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_identifier_read_identifier(seq![0x1Fu8], 0, 1)", + "witness": "read_identifier(seq![0x1Fu8], 0, 1) == Some((Tag { class: Class::Universal, constructed: false, number: 31 }, 1nat)) && !(seq![0x1Fu8].subrange(0, 1) =~= encode_identifier(Tag { class: Class::Universal, constructed: false, number: 31 }))" + } + }, + { + "id": "id-high-form-below-31", + "file": "src/spec.rs", + "find": "} else if number < 31 {", + "replace": "} else if number < 1 {", + "expect": "killed", + "reason": "Tag numbers 0 to 30 use the low form (8.1.2.2), so the high form with a number below 31 is not DER. Accepted, 1F 1E reads as tag 30 whose encoding is 1E: the canonicity lemma is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_identifier_read_identifier(seq![0x1Fu8, 0x1Eu8], 0, 2)", + "witness": "read_identifier(seq![0x1Fu8, 0x1Eu8], 0, 2) == Some((Tag { class: Class::Universal, constructed: false, number: 30 }, 2nat)) && !(seq![0x1Fu8, 0x1Eu8].subrange(0, 2) =~= encode_identifier(Tag { class: Class::Universal, constructed: false, number: 30 }))" + } + }, + { + "id": "id-leading-zero-digit", + "file": "src/spec.rs", + "find": "if b[pos as int + 1] % 128 == 0 {", + "replace": "if b[pos as int + 1] % 128 == 200 {", + "expect": "killed", + "reason": "8.1.2.4.2 c): the first subsequent octet's low seven bits are not all zero. Without the check, 1F 80 1F reads as tag 31 and re-encodes as 1F 1F: the canonicity lemma is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_identifier_read_identifier(seq![0x1Fu8, 0x80u8, 0x1Fu8], 0, 3)", + "witness": "read_identifier(seq![0x1Fu8, 0x80u8, 0x1Fu8], 0, 3) == Some((Tag { class: Class::Universal, constructed: false, number: 31 }, 3nat)) && !(seq![0x1Fu8, 0x80u8, 0x1Fu8].subrange(0, 3) =~= encode_identifier(Tag { class: Class::Universal, constructed: false, number: 31 }))" + } + }, + { + "id": "id-number-limit", + "file": "src/spec.rs", + "find": "} else if number > MAX_TAG_NUMBER as nat {", + "replace": "} else if number > 99999 as nat {", + "expect": "killed", + "reason": "The decoder rejects tag numbers of 2^14 and above (a stated limit). Without it, 1F 81 80 00 reads as tag 16384, and lemma_encode_identifier_read_identifier, which says the number is at most MAX_TAG_NUMBER, is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_identifier_read_identifier(seq![0x1Fu8, 0x81u8, 0x80u8, 0x00u8], 0, 4)", + "witness": "read_identifier(seq![0x1Fu8, 0x81u8, 0x80u8, 0x00u8], 0, 4) == Some((Tag { class: Class::Universal, constructed: false, number: 16384 }, 4nat)) && 16384u32 > MAX_TAG_NUMBER" + } + }, + { + "id": "len-short-form-boundary", + "file": "src/spec.rs", + "find": " if f < 128 {\n // 1. 8.1.3.4", + "replace": " if f <= 128 {\n // 1. 8.1.3.4", + "expect": "killed", + "reason": "The short form is bit 8 zero (8.1.3.4). Taking 80 as a short length of 128 makes lemma_encode_length_read_length false: its encoding is 81 80, not 80.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_length_read_length(seq![0x80u8], 0, 1)", + "witness": "read_length(seq![0x80u8], 0, 1) == Some((128nat, 1nat)) && !(seq![0x80u8].subrange(0, 1) =~= encode_length(128))" + } + }, + { + "id": "len-indefinite-accepted", + "file": "src/spec.rs", + "find": "length encoding shall be used\".\n None\n } else if f == 255 {", + "replace": "length encoding shall be used\".\n Some((0, 1))\n } else if f == 255 {", + "expect": "killed", + "reason": "The indefinite form 80 is not DER (10.1). Read as length 0, 80 re-encodes as 00: the canonicity lemma is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_length_read_length(seq![0x80u8], 0, 1)", + "witness": "read_length(seq![0x80u8], 0, 1) == Some((0nat, 1nat)) && !(seq![0x80u8].subrange(0, 1) =~= encode_length(0))" + } + }, + { + "id": "len-octet-count-limit", + "file": "src/spec.rs", + "find": "if n > MAX_LENGTH_OCTETS as nat {", + "replace": "if n > 9 as nat {", + "expect": "killed", + "reason": "The decoder rejects long forms of more than 8 octets (a stated limit). With 9 allowed, 89 01 00.. reads length 2^64 and lemma_encode_length_read_length, which says the length is below 2^64, is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_length_read_length(seq![0x89u8, 0x01u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8], 0, 10)", + "witness": "read_length(seq![0x89u8, 0x01u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8, 0x00u8], 0, 10) == Some((18446744073709551616nat, 10nat)) && pow256(8) == 18446744073709551616nat" + } + }, + { + "id": "len-leading-zero", + "file": "src/spec.rs", + "find": "} else if b[pos as int + 1] == 0 {", + "replace": "} else if b[pos as int + 1] == 7 {", + "expect": "killed", + "reason": "10.1 needs the minimum number of octets: a leading zero octet is redundant. Accepted, 82 00 C9 reads length 201 and re-encodes as 81 C9: the canonicity lemma is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_length_read_length(seq![0x82u8, 0x00u8, 0xC9u8], 0, 3)", + "witness": "read_length(seq![0x82u8, 0x00u8, 0xC9u8], 0, 3) == Some((201nat, 3nat)) && !(seq![0x82u8, 0x00u8, 0xC9u8].subrange(0, 3) =~= encode_length(201))" + } + }, + { + "id": "len-long-form-fits-short", + "file": "src/spec.rs", + "find": " if v < 128 {\n // 8.1.3.3 NOTE", + "replace": " if v < 0 {\n // 8.1.3.3 NOTE", + "expect": "killed", + "reason": "A length that fits the short form must use it (10.1). Accepted, 81 26 reads 38 and re-encodes as 26: the canonicity lemma is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_length_read_length(seq![0x81u8, 0x26u8], 0, 2)", + "witness": "read_length(seq![0x81u8, 0x26u8], 0, 2) == Some((38nat, 2nat)) && !(seq![0x81u8, 0x26u8].subrange(0, 2) =~= encode_length(38))" + } + }, + { + "id": "len-octets-inside-end", + "file": "src/spec.rs", + "find": "} else if pos + 1 + n > end {", + "replace": "} else if pos + n > end {", + "expect": "killed", + "reason": "The subsequent length octets must lie inside the end: one octet too few checked reads past it, and lemma_encode_length_read_length, which says the length octets end by `end`, is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_length_read_length(seq![0x81u8, 0xC9u8], 0, 1)", + "witness": "read_length(seq![0x81u8, 0xC9u8], 0, 1) == Some((201nat, 2nat))" + } + }, + { + "id": "tlv-contents-inside-end", + "file": "src/spec.rs", + "find": "if cstart + len > end {", + "replace": "if cstart + len > end + 1 {", + "expect": "killed", + "reason": "The contents must lie inside the end. Allowed one octet beyond, 04 02 AA BB read with end 3 returns contents ending at 4, and lemma_encode_tlv_read_tlv, which says the value ends by `end`, is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_tlv_read_tlv(seq![0x04u8, 0x02u8, 0xAAu8, 0xBBu8], 0, 3)", + "witness": "read_tlv(seq![0x04u8, 0x02u8, 0xAAu8, 0xBBu8], 0, 3) == Some(Tlv { tag: Tag { class: Class::Universal, constructed: false, number: 4 }, cstart: 2, cend: 4 })" + } + }, + { + "id": "bool-true-any-nonzero", + "file": "src/spec.rs", + "find": " } else if c[0] == 255 {", + "replace": " } else if c[0] >= 1 {", + "expect": "killed", + "reason": "11.1: DER TRUE is FF; BER allows any non-zero octet (8.2.2). Accepted, 01 01 01 reads TRUE and re-encodes as 01 01 FF: the canonicity lemma is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_boolean_read_boolean(seq![0x01u8, 0x01u8, 0x01u8], 0, 3)", + "witness": "read_boolean(seq![0x01u8, 0x01u8, 0x01u8], 0, 3) == Some((true, 3nat)) && !(seq![0x01u8, 0x01u8, 0x01u8].subrange(0, 3) =~= encode_boolean(true))" + } + }, + { + "id": "bool-true-value", + "file": "src/spec.rs", + "find": " } else if c[0] == 255 {", + "replace": " } else if c[0] == 254 {", + "expect": "killed", + "reason": "TRUE is the octet FF (11.1). With another octet, the contents FF are not a boolean and lemma_boolean_contents is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_boolean_contents(true)", + "witness": "boolean_contents(seq![0xFFu8]) is None" + } + }, + { + "id": "bool-false-value", + "file": "src/spec.rs", + "find": " } else if c[0] == 0 {\n // 8.2.2", + "replace": " } else if c[0] == 1 {\n // 8.2.2", + "expect": "killed", + "reason": "FALSE is the octet 00 (8.2.2). With another octet, the contents 00 are not a boolean and lemma_boolean_contents is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_boolean_contents(false)", + "witness": "boolean_contents(seq![0x00u8]) is None" + } + }, + { + "id": "int-redundant-zero", + "file": "src/spec.rs", + "find": "(c[0] == 0 && c[1] < 128))", + "replace": "(c[0] == 0 && c[1] < 0))", + "expect": "killed", + "reason": "8.3.2 b): a leading 00 before an octet with bit 8 clear is redundant. Accepted, 02 02 00 7F reads the magnitude 7F and re-encodes as 02 01 7F: lemma_encode_integer_read_integer is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_integer_read_integer(seq![0x02u8, 0x02u8, 0x00u8, 0x7Fu8], 0, 4)", + "witness": "read_integer(seq![0x02u8, 0x02u8, 0x00u8, 0x7Fu8], 0, 4) == Some((false, 3nat, 4nat, 4nat)) && !(seq![0x02u8, 0x02u8, 0x00u8, 0x7Fu8].subrange(0, 4) =~= encode_integer_unsigned(seq![0x02u8, 0x02u8, 0x00u8, 0x7Fu8].subrange(3, 4)))" + } + }, + { + "id": "int-redundant-ff", + "file": "src/spec.rs", + "find": "(c[0] == 255 && c[1] >= 128)", + "replace": "(c[0] == 255 && c[1] >= 255)", + "expect": "killed", + "reason": "8.3.2 a): a leading FF before an octet with bit 8 set is redundant. Accepted, FF 80 (the value -128, which is 80 in one octet) is a valid INTEGER by the mutant but not by the Guide's minimal two's complement rule: lemma_integer_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_integer_equiv(seq![0xFFu8, 0x80u8])", + "witness": "integer_contents_ok(seq![0xFFu8, 0x80u8]) && !k_integer_ok(seq![0xFFu8, 0x80u8])" + } + }, + { + "id": "int-empty", + "file": "src/spec.rs", + "find": "pub open spec fn integer_contents_ok(c: Seq) -> bool {\n &&& c.len() >= 1", + "replace": "pub open spec fn integer_contents_ok(c: Seq) -> bool {\n &&& c.len() >= 0", + "expect": "killed", + "reason": "8.3.1: the contents are one or more octets. With none allowed, the empty contents are an INTEGER here and not in the Guide: lemma_integer_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_integer_equiv(Seq::::empty())", + "witness": "integer_contents_ok(Seq::::empty()) && !k_integer_ok(Seq::::empty())" + } + }, + { + "id": "int-negative-value", + "file": "src/spec.rs", + "find": "be_value(c) as int - pow256(c.len()) as int", + "replace": "be_value(c) as int - pow256(c.len()) as int + 1", + "expect": "killed", + "reason": "8.3.3: the value is the octets' number less 256^n when bit 8 of the first is set. Off by one, 80 has value -127 and the Guide's -128: lemma_integer_value_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_integer_value_equiv(seq![0x80u8])", + "witness": "integer_value(seq![0x80u8]) == -127 && k_integer_value(seq![0x80u8]) == -128" + } + }, + { + "id": "bits-unused-max", + "file": "src/spec.rs", + "find": " &&& c[0] <= 7\n", + "replace": " &&& c[0] <= 8\n", + "expect": "killed", + "reason": "8.6.2.2: the unused-bits count is 0 to 7. Allowing 8 accepts contents the Guide rejects: lemma_bitstring_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_bitstring_equiv(seq![0x08u8, 0x00u8])", + "witness": "bit_string_contents_ok(seq![0x08u8, 0x00u8]) && !k_bitstring_ok(seq![0x08u8, 0x00u8])" + } + }, + { + "id": "bits-empty-unused", + "file": "src/spec.rs", + "find": "&&& (c.len() == 1 ==> c[0] == 0)", + "replace": "&&& (c.len() == 1 ==> c[0] <= 7)", + "expect": "killed", + "reason": "8.6.2.3: an empty bit string has unused count 0. Allowing any, the contents 01 are accepted and not by the Guide: lemma_bitstring_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_bitstring_equiv(seq![0x01u8])", + "witness": "bit_string_contents_ok(seq![0x01u8]) && !k_bitstring_ok(seq![0x01u8])" + } + }, + { + "id": "bits-padding-zero", + "file": "src/spec.rs", + "find": "&&& (c.len() >= 2 ==> (c.last() as nat) % pow2_unused(c[0]) == 0)", + "replace": "&&& (c.len() >= 2 ==> (c.last() as nat) % pow2_unused(c[0]) < 200)", + "expect": "killed", + "reason": "11.2.1: the unused bits are zero. Unchecked, 06 6E 5D E0 (padding bits 100000) is accepted and the Guide's DER padding rule rejects it: lemma_bitstring_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_bitstring_equiv(seq![0x06u8, 0x6Eu8, 0x5Du8, 0xE0u8])", + "witness": "bit_string_contents_ok(seq![0x06u8, 0x6Eu8, 0x5Du8, 0xE0u8]) && !k_bitstring_ok(seq![0x06u8, 0x6Eu8, 0x5Du8, 0xE0u8])" + } + }, + { + "id": "bits-pow2-table", + "file": "src/spec.rs", + "find": " } else if u == 3 {\n 8", + "replace": " } else if u == 3 {\n 4", + "expect": "killed", + "reason": "Three unused bits are the low three bits, a multiple of 2^3 = 8. With 4, the last octet 04 (low bits 100) passes where the Guide's rule fails it: lemma_bitstring_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_bitstring_equiv(seq![0x03u8, 0x04u8])", + "witness": "bit_string_contents_ok(seq![0x03u8, 0x04u8]) && !k_bitstring_ok(seq![0x03u8, 0x04u8])" + } + }, + { + "id": "enc-bit-string-order", + "file": "src/spec.rs", + "find": "encode_tlv(universal_primitive(BIT_STRING), seq![unused] + bits)", + "replace": "encode_tlv(universal_primitive(BIT_STRING), bits + seq![unused])", + "expect": "killed", + "reason": "The unused-bits octet comes first (8.6.2). Written last, 03 02 00 80 re-encodes as 03 02 80 00: lemma_encode_bit_string_read_bit_string is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_bit_string_read_bit_string(seq![0x03u8, 0x02u8, 0x00u8, 0x80u8], 0, 4)", + "witness": "read_bit_string(seq![0x03u8, 0x02u8, 0x00u8, 0x80u8], 0, 4) == Some((0u8, 3nat, 4nat, 4nat)) && !(seq![0x03u8, 0x02u8, 0x00u8, 0x80u8].subrange(0, 4) =~= encode_bit_string(0, seq![0x03u8, 0x02u8, 0x00u8, 0x80u8].subrange(3, 4)))" + } + }, + { + "id": "null-with-contents", + "file": "src/spec.rs", + "find": "if t.tag == universal_primitive(NULL) && t.cstart == t.cend {", + "replace": "if t.tag == universal_primitive(NULL) && t.cstart <= t.cend {", + "expect": "killed", + "reason": "8.8.2: NULL has no contents octets. Accepted with contents, 05 01 00 reads as a NULL ending at 3, and lemma_encode_null_read_null, which says it ends at pos + 2, is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_null_read_null(seq![0x05u8, 0x01u8, 0x00u8], 0, 3)", + "witness": "read_null(seq![0x05u8, 0x01u8, 0x00u8], 0, 3) == Some(3nat)" + } + }, + { + "id": "enc-null-contents", + "file": "src/spec.rs", + "find": "encode_tlv(universal_primitive(NULL), seq![])", + "replace": "encode_tlv(universal_primitive(NULL), seq![0u8])", + "expect": "killed", + "reason": "NULL is 05 00 (8.8). Encoded with a contents octet, the bytes read as a NULL are not its encoding: lemma_encode_null_read_null is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_null_read_null(seq![0x05u8, 0x00u8], 0, 2)", + "witness": "read_null(seq![0x05u8, 0x00u8], 0, 2) == Some(2nat) && !(seq![0x05u8, 0x00u8].subrange(0, 2) =~= encode_null())" + } + }, + { + "id": "octets-constructed-accepted", + "file": "src/spec.rs", + "find": " if t.tag == universal_primitive(number) {\n Some((t.cstart, t.cend))", + "replace": " if t.tag.number == number {\n Some((t.cstart, t.cend))", + "expect": "killed", + "reason": "10.2: the constructed form of an octet string is not DER. Accepted, 24 00 reads as an OCTET STRING and re-encodes as 04 00: lemma_encode_octets_read_octets is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_octets_read_octets(seq![0x24u8, 0x00u8], 0, 2, 4)", + "witness": "read_octets(seq![0x24u8, 0x00u8], 0, 2, 4) == Some((2nat, 2nat)) && !(seq![0x24u8, 0x00u8].subrange(0, 2) =~= encode_octets(4, seq![0x24u8, 0x00u8].subrange(2, 2)))" + } + }, + { + "id": "enc-octets-primitive", + "file": "src/spec.rs", + "find": " encode_tlv(universal_primitive(number), s)", + "replace": " encode_tlv(universal_constructed(number), s)", + "expect": "killed", + "reason": "A string is primitive (10.2). Encoded constructed, the bytes read as a string are not its encoding: lemma_encode_octets_read_octets is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_octets_read_octets(seq![0x04u8, 0x00u8], 0, 2, 4)", + "witness": "read_octets(seq![0x04u8, 0x00u8], 0, 2, 4) == Some((2nat, 2nat)) && !(seq![0x04u8, 0x00u8].subrange(0, 2) =~= encode_octets(4, seq![0x04u8, 0x00u8].subrange(2, 2)))" + } + }, + { + "id": "sequence-primitive-accepted", + "file": "src/spec.rs", + "find": " if t.tag == universal_constructed(number) {", + "replace": " if t.tag.number == number {", + "expect": "killed", + "reason": "8.9.1: a SEQUENCE is constructed. Accepted primitive, 10 00 reads as a SEQUENCE and re-encodes as 30 00: lemma_encode_sequence_expect_sequence is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_sequence_expect_sequence(seq![0x10u8, 0x00u8], 0, 2)", + "witness": "expect_sequence(seq![0x10u8, 0x00u8], 0, 2) == Some((2nat, 2nat)) && !(seq![0x10u8, 0x00u8].subrange(0, 2) =~= encode_sequence(seq![0x10u8, 0x00u8].subrange(2, 2)))" + } + }, + { + "id": "enc-sequence-constructed", + "file": "src/spec.rs", + "find": " encode_tlv(universal_constructed(SEQUENCE), contents)", + "replace": " encode_tlv(universal_primitive(SEQUENCE), contents)", + "expect": "killed", + "reason": "8.9.1: a SEQUENCE is encoded constructed. Written primitive, 30 00 is not the encoding of what it reads as: lemma_encode_sequence_expect_sequence is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_sequence_expect_sequence(seq![0x30u8, 0x00u8], 0, 2)", + "witness": "expect_sequence(seq![0x30u8, 0x00u8], 0, 2) == Some((2nat, 2nat)) && !(seq![0x30u8, 0x00u8].subrange(0, 2) =~= encode_sequence(seq![0x30u8, 0x00u8].subrange(2, 2)))" + } + }, + { + "id": "context-class-application", + "file": "src/spec.rs", + "find": "if tag.class == Class::ContextSpecific && tag.number == number {", + "replace": "if tag.class == Class::Application && tag.number == number {", + "expect": "killed", + "reason": "A context-specific component has class bits 10 (Table 1). Matching the application class reads 41 00 as [1] and re-encodes it as 81 00: lemma_encode_context_read_optional_context is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_context_read_optional_context(seq![0x41u8, 0x00u8], 0, 2, 1)", + "witness": "read_optional_context(seq![0x41u8, 0x00u8], 0, 2, 1) == Some(Some((false, 2nat, 2nat))) && !(seq![0x41u8, 0x00u8].subrange(0, 2) =~= encode_context(1, false, seq![0x41u8, 0x00u8].subrange(2, 2)))" + } + }, + { + "id": "context-number-match", + "file": "src/spec.rs", + "find": "if tag.class == Class::ContextSpecific && tag.number == number {", + "replace": "if tag.class == Class::ContextSpecific && tag.number >= number {", + "expect": "killed", + "reason": "The component has the tag number asked for. Matching any larger one reads 82 00 as [1] and re-encodes it as 81 00: lemma_encode_context_read_optional_context is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_context_read_optional_context(seq![0x82u8, 0x00u8], 0, 2, 1)", + "witness": "read_optional_context(seq![0x82u8, 0x00u8], 0, 2, 1) == Some(Some((false, 2nat, 2nat))) && !(seq![0x82u8, 0x00u8].subrange(0, 2) =~= encode_context(1, false, seq![0x82u8, 0x00u8].subrange(2, 2)))" + } + }, + { + "id": "context-absent-at-end", + "file": "src/spec.rs", + "find": " // Nothing is left: the component is absent.\n Some(None)", + "replace": " // Nothing is left: the component is absent.\n Some(Some((false, 0, 0)))", + "expect": "killed", + "reason": "An OPTIONAL component is absent when nothing is left (8.9.3): reporting it present makes lemma_read_optional_context_absent false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_read_optional_context_absent(Seq::::empty(), 0, 0, 1)", + "witness": "read_optional_context(Seq::::empty(), 0, 0, 1) == Some(Some((false, 0nat, 0nat)))" + } + }, + { + "id": "explicit-must-be-constructed", + "file": "src/spec.rs", + "find": " if constructed {\n Some(Some((cstart, cend)))", + "replace": " if true {\n Some(Some((cstart, cend)))", + "expect": "killed", + "reason": "8.14.3: an EXPLICIT tag is constructed. Accepted primitive, 80 00 reads as an explicit [0] and re-encodes as A0 00: lemma_encode_context_read_optional_explicit is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_context_read_optional_explicit(seq![0x80u8, 0x00u8], 0, 2, 0)", + "witness": "read_optional_explicit(seq![0x80u8, 0x00u8], 0, 2, 0) == Some(Some((2nat, 2nat))) && !(seq![0x80u8, 0x00u8].subrange(0, 2) =~= encode_context(0, true, seq![0x80u8, 0x00u8].subrange(2, 2)))" + } + }, + { + "id": "oid-padding-digit", + "file": "src/spec.rs", + "find": " if c[0] == 128 {\n // 8.19.2", + "replace": " if c[0] == 129 {\n // 8.19.2", + "expect": "killed", + "reason": "8.19.2: the leading octet of a subidentifier is not 80. Allowed, 06 03 2A 80 01 reads {1 2 1} and re-encodes as 06 02 2A 01: lemma_encode_oid_read_oid is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_oid_read_oid(seq![0x06u8, 0x03u8, 0x2Au8, 0x80u8, 0x01u8], 0, 5)", + "witness": "read_oid(seq![0x06u8, 0x03u8, 0x2Au8, 0x80u8, 0x01u8], 0, 5) == Some((seq![1nat, 2nat, 1nat], 5nat)) && !(seq![0x06u8, 0x03u8, 0x2Au8, 0x80u8, 0x01u8].subrange(0, 5) =~= encode_oid(seq![1nat, 2nat, 1nat]))" + } + }, + { + "id": "oid-first-arc-one", + "file": "src/spec.rs", + "find": " (1, (v - 40) as nat)", + "replace": " (1, (v - 41) as nat)", + "expect": "killed", + "reason": "8.19.4: the first subidentifier is X * 40 + Y. Off by one for X = 1, 29 reads {1 0} and re-encodes as 28: lemma_encode_oid_read_oid is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_oid_read_oid(seq![0x06u8, 0x01u8, 0x29u8], 0, 3)", + "witness": "read_oid(seq![0x06u8, 0x01u8, 0x29u8], 0, 3) == Some((seq![1nat, 0nat], 3nat)) && !(seq![0x06u8, 0x01u8, 0x29u8].subrange(0, 3) =~= encode_oid(seq![1nat, 0nat]))" + } + }, + { + "id": "oid-first-arc-two", + "file": "src/spec.rs", + "find": " (2, (v - 80) as nat)", + "replace": " (2, (v - 81) as nat)", + "expect": "killed", + "reason": "8.19.4: for X = 2, Y is the first subidentifier less 80. Off by one, 51 reads {2 0} and re-encodes as 50: lemma_encode_oid_read_oid is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_oid_read_oid(seq![0x06u8, 0x01u8, 0x51u8], 0, 3)", + "witness": "read_oid(seq![0x06u8, 0x01u8, 0x51u8], 0, 3) == Some((seq![2nat, 0nat], 3nat)) && !(seq![0x06u8, 0x01u8, 0x51u8].subrange(0, 3) =~= encode_oid(seq![2nat, 0nat]))" + } + }, + { + "id": "oid-subidentifier-limit", + "file": "src/spec.rs", + "find": " } else if v >= 0x1_0000_0000_0000_0000 {\n // A limit of this project (see the header).", + "replace": " } else if v >= 0x2_0000_0000_0000_0000 {\n // A limit of this project (see the header).", + "expect": "killed", + "reason": "The decoder rejects subidentifiers of 2^64 and above (a stated limit). Raised, 82 80 80 80 80 80 80 80 80 00 reads 2^64 and lemma_encode_subidentifiers_decode, which says each is below 2^64, is false.", + "certificate": { + "file": "src/lemmas_oid.rs", + "lemma": "lemma_encode_subidentifiers_decode(seq![0x82u8, 0x80u8, 0x80u8, 0x80u8, 0x80u8, 0x80u8, 0x80u8, 0x80u8, 0x80u8, 0x00u8])", + "witness": "subidentifiers(seq![0x82u8, 0x80u8, 0x80u8, 0x80u8, 0x80u8, 0x80u8, 0x80u8, 0x80u8, 0x80u8, 0x00u8]) == Some(seq![18446744073709551616nat])" + } + }, + { + "id": "oid-series-end", + "file": "src/spec.rs", + "find": " } else if b[i as int] < 128 {\n Some(i)", + "replace": " } else if b[i as int] < 127 {\n Some(i)", + "expect": "killed", + "reason": "Bit 8 of the last octet of a series is zero (8.19.2). Treating 7F as a continuation reads 2A 7F 01 as {1 2 16257} and re-encodes as 2A FF 01: lemma_encode_oid_read_oid is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_oid_read_oid(seq![0x06u8, 0x03u8, 0x2Au8, 0x7Fu8, 0x01u8], 0, 5)", + "witness": "read_oid(seq![0x06u8, 0x03u8, 0x2Au8, 0x7Fu8, 0x01u8], 0, 5) == Some((seq![1nat, 2nat, 16257nat], 5nat)) && !(seq![0x06u8, 0x03u8, 0x2Au8, 0x7Fu8, 0x01u8].subrange(0, 5) =~= encode_oid(seq![1nat, 2nat, 16257nat]))" + } + }, + { + "id": "enc-subidentifier-continuation", + "file": "src/spec.rs", + "find": "subidentifier_prefix(w / 128) + seq![(128 + w % 128) as u8]", + "replace": "subidentifier_prefix(w / 128) + seq![(w % 128) as u8]", + "expect": "killed", + "reason": "Bit 8 is one on every octet of a subidentifier but the last (8.19.2). Without it, 200 encodes as 01 48, not 81 48: lemma_encode_subidentifiers_decode is false.", + "certificate": { + "file": "src/lemmas_oid.rs", + "lemma": "lemma_encode_subidentifiers_decode(seq![0x81u8, 0x48u8])", + "witness": "subidentifiers(seq![0x81u8, 0x48u8]) == Some(seq![200nat]) && !(encode_subidentifiers(seq![200nat]) =~= seq![0x81u8, 0x48u8])" + } + }, + { + "id": "enc-oid-first-subidentifier", + "file": "src/spec.rs", + "find": "encode_subidentifiers(seq![40 * arcs[0] + arcs[1]] + arcs.skip(2))", + "replace": "encode_subidentifiers(seq![39 * arcs[0] + arcs[1]] + arcs.skip(2))", + "expect": "killed", + "reason": "8.19.4: the first subidentifier is (X * 40) + Y. With 39, {1 2} encodes as 29, not 2A: lemma_oid_contents_oid_components is false.", + "certificate": { + "file": "src/lemmas_oid.rs", + "lemma": "lemma_oid_contents_oid_components(seq![0x2Au8])", + "witness": "oid_components(seq![0x2Au8]) == Some(seq![1nat, 2nat]) && !(oid_contents(seq![1nat, 2nat]) =~= seq![0x2Au8])" + } + }, + { + "id": "enc-length-octets", + "file": "src/spec.rs", + "find": " } else if len < 0x1_0000 {\n 2", + "replace": " } else if len < 0x2_0000 {\n 2", + "expect": "killed", + "reason": "10.1: the fewest octets that hold the length. 65536 does not fit two octets: lemma_encode_length_shortest, which says the length is below 256^w, is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_length_shortest(65536)", + "witness": "length_octets(65536) == 2 && pow256(2) == 65536 && pow256(8) == 18446744073709551616nat" + } + }, + { + "id": "enc-length-short-form", + "file": "src/spec.rs", + "find": " if len < 128 {\n // 8.1.3.4.", + "replace": " if len < 127 {\n // 8.1.3.4.", + "expect": "killed", + "reason": "127 fits the short form (8.1.3.3 NOTE) and 10.1 requires it: encoded in the long form, lemma_encode_length_shortest is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_length_shortest(127)", + "witness": "encode_length(127) == seq![0x81u8, 0x7Fu8] && pow256(8) == 18446744073709551616nat" + } + }, + { + "id": "enc-class-context", + "file": "src/spec.rs", + "find": " Class::ContextSpecific => 2,", + "replace": " Class::ContextSpecific => 3,", + "expect": "killed", + "reason": "Table 1: context-specific is bits 10. Encoded as 11, 80 reads as a context-specific [0] and re-encodes as C0: the canonicity lemma is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_identifier_read_identifier(seq![0x80u8], 0, 1)", + "witness": "read_identifier(seq![0x80u8], 0, 1) == Some((Tag { class: Class::ContextSpecific, constructed: false, number: 0 }, 1nat)) && !(seq![0x80u8].subrange(0, 1) =~= encode_identifier(Tag { class: Class::ContextSpecific, constructed: false, number: 0 }))" + } + }, + { + "id": "enc-high-tag-form", + "file": "src/spec.rs", + "find": " } else if tag.number < 128 {", + "replace": " } else if tag.number < 127 {", + "expect": "killed", + "reason": "8.1.2.4.2 c): no leading zero digit. Tag 127 encoded with two subsequent octets starts with 80: the bytes 1F 7F read as tag 127 are not its encoding, and the canonicity lemma is false.", + "certificate": { + "file": "src/lemmas_tlv.rs", + "lemma": "lemma_encode_identifier_read_identifier(seq![0x1Fu8, 0x7Fu8], 0, 2)", + "witness": "read_identifier(seq![0x1Fu8, 0x7Fu8], 0, 2) == Some((Tag { class: Class::Universal, constructed: false, number: 127 }, 2nat)) && !(seq![0x1Fu8, 0x7Fu8].subrange(0, 2) =~= encode_identifier(Tag { class: Class::Universal, constructed: false, number: 127 }))" + } + }, + { + "id": "enc-boolean-true", + "file": "src/spec.rs", + "find": "encode_tlv(universal_primitive(BOOLEAN), seq![if v { 0xFFu8 } else { 0x00u8 }])", + "replace": "encode_tlv(universal_primitive(BOOLEAN), seq![if v { 0x01u8 } else { 0x00u8 }])", + "expect": "killed", + "reason": "11.1: DER encodes TRUE as FF. Encoded as 01, 01 01 FF read as TRUE is not the encoding of TRUE: the canonicity lemma is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_encode_boolean_read_boolean(seq![0x01u8, 0x01u8, 0xFFu8], 0, 3)", + "witness": "read_boolean(seq![0x01u8, 0x01u8, 0xFFu8], 0, 3) == Some((true, 3nat)) && !(seq![0x01u8, 0x01u8, 0xFFu8].subrange(0, 3) =~= encode_boolean(true))" + } + }, + { + "id": "enc-integer-pad", + "file": "src/spec.rs", + "find": " } else if s[0] >= 128 {\n seq![0u8] + s", + "replace": " } else if s[0] >= 129 {\n seq![0u8] + s", + "expect": "killed", + "reason": "8.3.3: a positive number whose first octet has bit 8 set needs a leading 00, or it reads as negative. Without it for 80, lemma_integer_unsigned_contents, which says the contents are non-negative, is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_integer_unsigned_contents(seq![0x80u8])", + "witness": "integer_unsigned_contents(seq![0x80u8]) == seq![0x80u8]" + } + }, + { + "id": "enc-integer-strip", + "file": "src/spec.rs", + "find": " if s.len() > 0 && s[0] == 0 {\n strip_leading_zeros", + "replace": " if s.len() > 0 && s[0] == 1 {\n strip_leading_zeros", + "expect": "killed", + "reason": "Only zero octets are stripped before an INTEGER's magnitude. Stripping a 01 changes the number: lemma_strip_leading_zeros_value is false.", + "certificate": { + "file": "src/lemmas_typed.rs", + "lemma": "lemma_strip_leading_zeros_value(seq![0x01u8, 0x02u8])", + "witness": "strip_leading_zeros(seq![0x01u8, 0x02u8]) == seq![0x02u8] && be_value(seq![0x02u8]) == 2 && be_value(seq![0x01u8, 0x02u8]) == 258" + } + }, + { + "id": "tag-integer", + "file": "src/spec.rs", + "find": "pub const INTEGER: u32 = 2;", + "replace": "pub const INTEGER: u32 = 7;", + "expect": "killed", + "reason": "The universal tag number of INTEGER is 2 (X.680 Table 1, and Kaliski's examples). Changed, the decoder and encoder still agree with each other but not with the Guide: lemma_read_integer_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_read_integer_equiv(seq![0x02u8, 0x01u8, 0x00u8], 0, 3)", + "witness": "read_integer(seq![0x02u8, 0x01u8, 0x00u8], 0, 3) is None && k_read_integer(seq![0x02u8, 0x01u8, 0x00u8], 0, 3) is Some" + } + }, + { + "id": "tag-bit-string", + "file": "src/spec.rs", + "find": "pub const BIT_STRING: u32 = 3;", + "replace": "pub const BIT_STRING: u32 = 8;", + "expect": "killed", + "reason": "The universal tag number of BIT STRING is 3 (X.680 Table 1, and Kaliski's examples). Changed, the decoder and encoder still agree with each other but not with the Guide: lemma_read_bit_string_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_read_bit_string_equiv(seq![0x03u8, 0x01u8, 0x00u8], 0, 3)", + "witness": "read_bit_string(seq![0x03u8, 0x01u8, 0x00u8], 0, 3) is None && k_read_bit_string(seq![0x03u8, 0x01u8, 0x00u8], 0, 3) is Some" + } + }, + { + "id": "tag-null", + "file": "src/spec.rs", + "find": "pub const NULL: u32 = 5;", + "replace": "pub const NULL: u32 = 9;", + "expect": "killed", + "reason": "The universal tag number of NULL is 5 (X.680 Table 1, and Kaliski's examples). Changed, the decoder and encoder still agree with each other but not with the Guide: lemma_read_null_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_read_null_equiv(seq![0x05u8, 0x00u8], 0, 2)", + "witness": "read_null(seq![0x05u8, 0x00u8], 0, 2) is None && k_read_null(seq![0x05u8, 0x00u8], 0, 2) is Some" + } + }, + { + "id": "tag-octet-string", + "file": "src/spec.rs", + "find": "pub const OCTET_STRING: u32 = 4;", + "replace": "pub const OCTET_STRING: u32 = 9;", + "expect": "killed", + "reason": "The universal tag number of OCTET STRING is 4 (X.680 Table 1, and Kaliski's examples). Changed, the decoder and encoder still agree with each other but not with the Guide: lemma_read_octet_string_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_read_octet_string_equiv(seq![0x04u8, 0x00u8], 0, 2)", + "witness": "read_octet_string(seq![0x04u8, 0x00u8], 0, 2) is None && k_read_string(seq![0x04u8, 0x00u8], 0, 2, 4) is Some" + } + }, + { + "id": "tag-printable-string", + "file": "src/spec.rs", + "find": "pub const PRINTABLE_STRING: u32 = 19;", + "replace": "pub const PRINTABLE_STRING: u32 = 9;", + "expect": "killed", + "reason": "The universal tag number of PrintableString is 19 (X.680 Table 1, and Kaliski's examples). Changed, the decoder and encoder still agree with each other but not with the Guide: lemma_read_printable_string_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_read_printable_string_equiv(seq![0x13u8, 0x00u8], 0, 2)", + "witness": "read_printable_string(seq![0x13u8, 0x00u8], 0, 2) is None && k_read_string(seq![0x13u8, 0x00u8], 0, 2, 19) is Some" + } + }, + { + "id": "tag-ia5-string", + "file": "src/spec.rs", + "find": "pub const IA5_STRING: u32 = 22;", + "replace": "pub const IA5_STRING: u32 = 9;", + "expect": "killed", + "reason": "The universal tag number of IA5String is 22 (X.680 Table 1, and Kaliski's examples). Changed, the decoder and encoder still agree with each other but not with the Guide: lemma_read_ia5_string_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_read_ia5_string_equiv(seq![0x16u8, 0x00u8], 0, 2)", + "witness": "read_ia5_string(seq![0x16u8, 0x00u8], 0, 2) is None && k_read_string(seq![0x16u8, 0x00u8], 0, 2, 22) is Some" + } + }, + { + "id": "tag-utc-time", + "file": "src/spec.rs", + "find": "pub const UTC_TIME: u32 = 23;", + "replace": "pub const UTC_TIME: u32 = 9;", + "expect": "killed", + "reason": "The universal tag number of UTCTime is 23 (X.680 Table 1, and Kaliski's examples). Changed, the decoder and encoder still agree with each other but not with the Guide: lemma_read_utc_time_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_read_utc_time_equiv(seq![0x17u8, 0x00u8], 0, 2)", + "witness": "read_utc_time(seq![0x17u8, 0x00u8], 0, 2) is None && k_read_string(seq![0x17u8, 0x00u8], 0, 2, 23) is Some" + } + }, + { + "id": "tag-sequence", + "file": "src/spec.rs", + "find": "pub const SEQUENCE: u32 = 16;", + "replace": "pub const SEQUENCE: u32 = 9;", + "expect": "killed", + "reason": "The universal tag number of SEQUENCE is 16 (X.680 Table 1, and Kaliski's examples). Changed, the decoder and encoder still agree with each other but not with the Guide: lemma_expect_sequence_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_expect_sequence_equiv(seq![0x30u8, 0x00u8], 0, 2)", + "witness": "expect_sequence(seq![0x30u8, 0x00u8], 0, 2) is None && k_expect_universal(seq![0x30u8, 0x00u8], 0, 2, true, 16) is Some" + } + }, + { + "id": "tag-set", + "file": "src/spec.rs", + "find": "pub const SET: u32 = 17;", + "replace": "pub const SET: u32 = 9;", + "expect": "killed", + "reason": "The universal tag number of SET is 17 (X.680 Table 1, and Kaliski's examples). Changed, the decoder and encoder still agree with each other but not with the Guide: lemma_expect_set_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_expect_set_equiv(seq![0x31u8, 0x00u8], 0, 2)", + "witness": "expect_set(seq![0x31u8, 0x00u8], 0, 2) is None && k_expect_universal(seq![0x31u8, 0x00u8], 0, 2, true, 17) is Some" + } + }, + { + "id": "tag-object-identifier", + "file": "src/spec.rs", + "find": "pub const OBJECT_IDENTIFIER: u32 = 6;", + "replace": "pub const OBJECT_IDENTIFIER: u32 = 9;", + "expect": "killed", + "reason": "The universal tag number of OBJECT IDENTIFIER is 6 (X.680 Table 1, and Kaliski's examples). Changed, the decoder and encoder still agree with each other but not with the Guide: lemma_read_oid_equiv is false.", + "certificate": { + "file": "src/independent.rs", + "lemma": "lemma_read_oid_equiv(seq![0x06u8, 0x01u8, 0x2Au8], 0, 3)", + "witness": "read_tlv(seq![0x06u8, 0x01u8, 0x2Au8], 0, 3) == Some(Tlv { tag: Tag { class: Class::Universal, constructed: false, number: 6 }, cstart: 2, cend: 3 }) && read_oid(seq![0x06u8, 0x01u8, 0x2Au8], 0, 3) is None && k_read_oid(seq![0x06u8, 0x01u8, 0x2Au8], 0, 3) is Some" + } + } + ] +} diff --git a/crates/clazel-der/src/contract.rs b/crates/clazel-der/src/contract.rs new file mode 100644 index 0000000..1b9ebd6 --- /dev/null +++ b/crates/clazel-der/src/contract.rs @@ -0,0 +1,166 @@ +//! Contracts the executable DER decoder and encoder must be proven to meet. +//! Each exec function's `ensures` clause is exactly one of these predicates +//! (with its result mapped to the spec's types by the views in +//! `decode.rs`), so reviewing these and [`super::spec`] is enough to know +//! what the implementation guarantees. +//! +//! Every reader takes the input `input`, a position `pos` and an `end` with +//! `end <= input.len()`, and is proven equal to the spec reader of the same +//! name on `(input@, pos, end)`: it returns exactly what the spec returns, +//! and `None` exactly when the spec does. + +#[allow(unused_imports)] +use super::spec::*; +#[allow(unused_imports)] +use vstd::prelude::*; + +verus! { + +/// Precondition of every reader: `end` is inside the input, `pos` is not +/// past it (a position beyond `end` is "nothing left", which every reader +/// answers with `None`, or `Some(None)` for an optional component). +pub open spec fn read_requires(input_len: nat, end: nat) -> bool { + end <= input_len +} + +/// Contract for `read_identifier`: the tag and the number of identifier +/// octets, as in [`read_identifier`]. +pub open spec fn read_identifier_ensures(input: Seq, pos: nat, end: nat, out: Option<(Tag, nat)>) -> bool { + out == read_identifier(input, pos, end) +} + +/// Contract for `read_length`: the length and the number of length octets, +/// as in [`read_length`]. +pub open spec fn read_length_ensures(input: Seq, pos: nat, end: nat, out: Option<(nat, nat)>) -> bool { + out == read_length(input, pos, end) +} + +/// Contract for `read_tlv`: the tag and contents range, as in [`read_tlv`]. +pub open spec fn read_tlv_ensures(input: Seq, pos: nat, end: nat, out: Option) -> bool { + out == read_tlv(input, pos, end) +} + +/// Contract for `read_boolean`. +pub open spec fn read_boolean_ensures(input: Seq, pos: nat, end: nat, out: Option<(bool, nat)>) -> bool { + out == read_boolean(input, pos, end) +} + +/// Contract for `read_integer`: sign, magnitude range, next position. +pub open spec fn read_integer_ensures(input: Seq, pos: nat, end: nat, out: Option<(bool, nat, nat, nat)>) -> bool { + out == read_integer(input, pos, end) +} + +/// Contract for `read_bit_string`: unused bits, the range of the octets that +/// hold the bits, next position. +pub open spec fn read_bit_string_ensures(input: Seq, pos: nat, end: nat, out: Option<(u8, nat, nat, nat)>) -> bool { + out == read_bit_string(input, pos, end) +} + +/// Contract for `read_octets` and the string readers built on it: the +/// contents range. +pub open spec fn read_octets_ensures(input: Seq, pos: nat, end: nat, number: u32, out: Option<(nat, nat)>) -> bool { + out == read_octets(input, pos, end, number) +} + +/// Contract for `read_null`: the next position. +pub open spec fn read_null_ensures(input: Seq, pos: nat, end: nat, out: Option) -> bool { + out == read_null(input, pos, end) +} + +/// Contract for `read_oid`: the components and the next position. +pub open spec fn read_oid_ensures(input: Seq, pos: nat, end: nat, out: Option<(Seq, nat)>) -> bool { + out == read_oid(input, pos, end) +} + +/// Contract on what `read_oid` allocates: it never holds more components +/// than the OBJECT IDENTIFIER has contents octets plus one, so a declared +/// length of 2^64 reserves nothing (the vector grows only by components it +/// has read). +pub open spec fn read_oid_alloc_ensures(input: Seq, pos: nat, end: nat, out: Option) -> bool { + match out { + Some(n) => match read_tlv(input, pos, end) { + Some(t) => n <= t.cend - t.cstart + 1, + None => false, + }, + None => true, + } +} + +/// Contract for `expect_constructed`, `expect_sequence` and `expect_set`: +/// the contents range. +pub open spec fn expect_constructed_ensures(input: Seq, pos: nat, end: nat, number: u32, out: Option<(nat, nat)>) -> bool { + out == expect_constructed(input, pos, end, number) +} + +/// Contract for `read_optional_context`. +pub open spec fn read_optional_context_ensures( + input: Seq, + pos: nat, + end: nat, + number: u32, + out: Option>, +) -> bool { + out == read_optional_context(input, pos, end, number) +} + +/// Contract for `read_optional_explicit`. +pub open spec fn read_optional_explicit_ensures( + input: Seq, + pos: nat, + end: nat, + number: u32, + out: Option>, +) -> bool { + out == read_optional_explicit(input, pos, end, number) +} + +/// Contract for `expect_end`. +pub open spec fn expect_end_ensures(pos: nat, end: nat, out: bool) -> bool { + out == expect_end(pos, end) +} + +/// Precondition of `encode_tlv`: a tag number the decoder accepts. (The +/// contents are a slice, so shorter than 2^64 octets, which is the other +/// half of [`tlv_ok`]; the function proves that itself.) +pub open spec fn encode_tlv_requires(tag: Tag) -> bool { + tag.number <= MAX_TAG_NUMBER +} + +/// Contract for `encode_tlv`: identifier, length, contents. +pub open spec fn encode_tlv_ensures(tag: Tag, contents: Seq, out: Seq) -> bool { + out == encode_tlv(tag, contents) +} + +/// Precondition of `encode_length`: the length fits at most 8 octets. +pub open spec fn encode_length_requires(len: nat) -> bool { + len < pow256(8) +} + +/// Contract for `encode_length`: the shortest form. +pub open spec fn encode_length_ensures(len: nat, out: Seq) -> bool { + out == encode_length(len) +} + +/// Contract on what the encoders allocate: the output is the contents plus +/// at most 12 octets (3 identifier octets and 9 length octets), never more. +pub open spec fn encode_alloc_ensures(contents_len: nat, out_len: nat) -> bool { + out_len <= contents_len + 12 +} + +/// Precondition of `encode_oid`: components that [`oid_valid`] accepts. +pub open spec fn encode_oid_requires(arcs: Seq) -> bool { + oid_valid(arcs) +} + +/// Contract for `encode_oid`. +pub open spec fn encode_oid_ensures(arcs: Seq, out: Seq) -> bool { + out == encode_oid(arcs) +} + +/// Contract for `encode_integer_unsigned`: the INTEGER of the number the +/// magnitude octets spell. +pub open spec fn encode_integer_unsigned_ensures(magnitude: Seq, out: Seq) -> bool { + out == encode_integer_unsigned(magnitude) +} + +} // verus! diff --git a/crates/clazel-der/src/decode.rs b/crates/clazel-der/src/decode.rs new file mode 100644 index 0000000..ba61efb --- /dev/null +++ b/crates/clazel-der/src/decode.rs @@ -0,0 +1,889 @@ +//! The executable decoder, proven to meet [`super::contract`]. +//! +//! Each function follows the spec function of the same name in +//! [`super::spec`] step by step (the clause comments and quotes live there). +//! Every reader is a cursor: it takes the input, a position and an end with +//! `end <= input.len()`, and returns where the value it read stops. None of +//! them reserves memory for a declared length: they return ranges, and the +//! one that builds a vector ([`read_oid`]) pushes only components it has +//! read. + +#[allow(unused_imports)] +use super::spec::*; +#[allow(unused_imports)] +use vstd::prelude::*; + +verus! { + +/// A data value's extent: the exec twin of [`super::spec::Tlv`], whose +/// view it is. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Tlv { + /// The tag from the identifier octets. + pub tag: Tag, + /// Index of the first contents octet. + pub cstart: usize, + /// Index after the last contents octet, which is where the value ends. + pub cend: usize, +} + +impl View for Tlv { + type V = super::spec::Tlv; + + open spec fn view(&self) -> super::spec::Tlv { + super::spec::Tlv { tag: self.tag, cstart: self.cstart as nat, cend: self.cend as nat } + } +} + +/// The spec view of an optional extent. +pub open spec fn view_tlv(r: Option) -> Option { + match r { + Some(t) => Some(t@), + None => None, + } +} + +/// The spec view of a tag and a length in octets. +pub open spec fn view_tag_len(r: Option<(Tag, usize)>) -> Option<(Tag, nat)> { + match r { + Some((t, n)) => Some((t, n as nat)), + None => None, + } +} + +/// The spec view of a length and the number of length octets. +pub open spec fn view_length(r: Option<(u64, usize)>) -> Option<(nat, nat)> { + match r { + Some((v, n)) => Some((v as nat, n as nat)), + None => None, + } +} + +/// The spec view of a pair of positions. +pub open spec fn view_range(r: Option<(usize, usize)>) -> Option<(nat, nat)> { + match r { + Some((a, b)) => Some((a as nat, b as nat)), + None => None, + } +} + +/// §8.1.3.5: the `width` octets at `pos` as an unsigned integer base 256. +/// The recursion is [`be_value`]'s: the last octet is the least +/// significant. +pub(crate) fn read_be(input: &[u8], pos: usize, width: usize) -> (value: u64) + requires + width <= 8, + pos + width <= input.len(), + ensures + value as nat == be_value(input@.subrange(pos as int, pos as int + width as int)), + decreases width, +{ + if width == 0 { + assert(input@.subrange(pos as int, pos as int) =~= Seq::::empty()); + return 0; + } + let ghost whole = input@.subrange(pos as int, pos as int + width as int); + let ghost init = input@.subrange(pos as int, pos as int + width as int - 1); + assert(whole.drop_last() =~= init); + // Never saturates: width >= 1. + let before = width.saturating_sub(1); + let high = read_be(input, pos, before); + proof { + super::lemmas::lemma_be_value_bound(init); + super::lemmas::lemma_pow256_at_most_8(width as nat); + assert(pow256(width as nat) == 256 * pow256(before as nat)); + assert(high as nat * 256 + 255 <= 0xFFFF_FFFF_FFFF_FFFF); + } + // Never saturates: pos + width <= input.len(), so the index is in range. + let index = pos.saturating_add(before); + let last = match input.get(index) { + Some(&b) => b, + None => 0, + }; + assert(whole.last() == input@[index as int]); + // Never saturates: high * 256 + last <= 2^64 - 1 (shown above). + high.saturating_mul(256).saturating_add(last as u64) +} + +/// §8.1.2.4.2 and §8.19.2: the series of octets at `start` (bit 8 set on +/// every octet but the last) and the number its low seven bits spell +/// ([`base128`]), if that number is at most `limit`. Returns the number and +/// the index of the last octet of the series. `None` when there is no last +/// octet in `[start, end)`, or when the number exceeds `limit`; the spec +/// functions that call it answer `None` in both cases. +pub(crate) fn read_series(input: &[u8], start: usize, end: usize, limit: u64) -> (r: Option<(u64, usize)>) + requires + end <= input.len(), + ensures + match r { + Some((v, j)) => { + &&& series_end(input@, start as nat, end as nat) == Some(j as nat) + &&& v as nat == base128(input@.subrange(start as int, j as int + 1)) + &&& v <= limit + }, + None => match series_end(input@, start as nat, end as nat) { + Some(j) => base128(input@.subrange(start as int, j as int + 1)) > limit, + None => true, + }, + }, +{ + if start >= end { + return None; + } + let mut i = start; + let mut acc: u128 = 0; + proof { + assert(input@.subrange(start as int, start as int) =~= Seq::::empty()); + } + while i < end + invariant + start <= i <= end, + end <= input.len(), + acc <= limit as u128, + acc as nat == base128(input@.subrange(start as int, i as int)), + forall|k: int| start <= k < i ==> #[trigger] input@[k] >= 128, + decreases end - i, + { + let o = match input.get(i) { + Some(&b) => b, + None => return None, + }; + let ghost before = input@.subrange(start as int, i as int); + let ghost after = input@.subrange(start as int, i as int + 1); + proof { + assert(after =~= before.push(o)); + super::lemmas::lemma_base128_push(before, o); + } + // acc <= limit < 2^64, so acc * 128 + 127 < 2^71 and this never + // saturates. + let acc2 = acc.saturating_mul(128).saturating_add((o % 128) as u128); + if acc2 > limit as u128 { + proof { + super::lemmas::lemma_series_end_skip(input@, start as nat, i as nat, end as nat); + super::lemmas::lemma_series_end_in_range(input@, i as nat, end as nat); + if let Some(j) = series_end(input@, i as nat, end as nat) { + super::lemmas::lemma_base128_monotone(input@, start as int, i as int + 1, j as int + 1); + } + } + return None; + } + if o >= 128 { + acc = acc2; + // Never saturates: i < end. + i = i.saturating_add(1); + } else { + proof { + super::lemmas::lemma_series_end_skip(input@, start as nat, i as nat, end as nat); + } + return Some((acc2 as u64, i)); + } + } + proof { + super::lemmas::lemma_series_end_skip(input@, start as nat, i as nat, end as nat); + } + None +} + +/// Table 1: the class encoded by bits 8 and 7. +fn class_of(bits: u8) -> (c: Class) + ensures + c == class_from_bits(bits), +{ + if bits == 0 { + Class::Universal + } else if bits == 1 { + Class::Application + } else if bits == 2 { + Class::ContextSpecific + } else { + Class::Private + } +} + +/// §8.1.2: the identifier octets at `pos`: the tag and their number. +pub fn read_identifier(input: &[u8], pos: usize, end: usize) -> (r: Option<(Tag, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + super::contract::read_identifier_ensures(input@, pos as nat, end as nat, view_tag_len(r)), +{ + if pos >= end { + return None; + } + let o = match input.get(pos) { + Some(&b) => b, + None => return None, + }; + let class = class_of(o / 64); + let constructed = (o / 32) % 2 == 1; + if o % 32 != 31 { + // 1. + return Some((Tag { class, constructed, number: (o % 32) as u32 }, 1)); + } + // 2. Never saturates: pos < end <= input.len(). + let first = pos.saturating_add(1); + let (number, j) = read_series(input, first, end, MAX_TAG_NUMBER as u64)?; + proof { + super::lemmas::lemma_series_end_in_range(input@, first as nat, end as nat); + } + // 8.1.2.4.2 c): the first subsequent octet is at `first <= j < end`. + match input.get(first) { + Some(&f) => { + if f % 128 == 0 { + return None; + } + }, + None => return None, + } + if number < 31 { + return None; + } + // number <= MAX_TAG_NUMBER, so this is exact. j < end <= input.len() and + // j >= first > pos: the length is j + 1 - pos, never saturating. + Some((Tag { class, constructed, number: number as u32 }, j.saturating_add(1).saturating_sub(pos))) +} + +/// §8.1.3 and §10.1: the length octets at `pos`: the length and their +/// number. +pub fn read_length(input: &[u8], pos: usize, end: usize) -> (r: Option<(u64, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + super::contract::read_length_ensures(input@, pos as nat, end as nat, view_length(r)), +{ + if pos >= end { + return None; + } + let f = match input.get(pos) { + Some(&b) => b, + None => return None, + }; + if f < 128 { + // 1. + return Some((f as u64, 1)); + } + if f == 128 { + // 2. + return None; + } + if f == 255 { + // 3. + return None; + } + // 4. Never saturates: f >= 128. + let n = f.saturating_sub(128) as usize; + if n > 8 { + return None; + } + // end > pos, so this is end - pos - 1 exactly. + let rest = end.saturating_sub(pos).saturating_sub(1); + if n > rest { + return None; + } + // Never saturates: pos + 1 <= end. + let first = pos.saturating_add(1); + match input.get(first) { + Some(&b) => { + if b == 0 { + return None; + } + }, + None => return None, + } + let v = read_be(input, first, n); + if v < 128 { + return None; + } + // Never saturates: n <= 8. + Some((v, n.saturating_add(1))) +} + +/// §8.1.1.1: the identifier octets, the length octets, and the contents +/// octets inside `[pos, end)`. +pub fn read_tlv(input: &[u8], pos: usize, end: usize) -> (r: Option) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + super::contract::read_tlv_ensures(input@, pos as nat, end as nat, view_tlv(r)), + match r { + Some(t) => pos < t.cstart && t.cstart <= t.cend && t.cend <= end, + None => true, + }, +{ + proof { + super::lemmas::lemma_read_tlv_bounds(input@, pos as nat, end as nat); + } + let (tag, id_len) = read_identifier(input, pos, end)?; + // pos < end and id_len octets were read inside [pos, end). + let after_id = pos.saturating_add(id_len); + let (len, len_len) = read_length(input, after_id, end)?; + // The length octets lie inside [after_id, end). + let cstart = after_id.saturating_add(len_len); + // cstart <= end, so this is end - cstart exactly. + let room = end.saturating_sub(cstart); + if len > room as u64 { + return None; + } + // len <= room, so this is exact and never saturates. + let cend = cstart.saturating_add(len as usize); + Some(Tlv { tag, cstart, cend }) +} + +/// The end of contents: nothing is left in `[pos, end)`. +pub fn expect_end(pos: usize, end: usize) -> (r: bool) + ensures + super::contract::expect_end_ensures(pos as nat, end as nat, r), +{ + pos == end +} + + +/// Whether `tag` is the universal type `number`, in the given form. +fn is_universal(tag: Tag, constructed: bool, number: u32) -> (r: bool) + ensures + r == (tag == Tag { class: Class::Universal, constructed, number }), +{ + match tag.class { + Class::Universal => tag.constructed == constructed && tag.number == number, + _ => false, + } +} + +/// The octet at `index`, or 0 where there is none (the callers' bounds make +/// that unreachable). +pub(crate) fn octet_at(input: &[u8], index: usize) -> (o: u8) + ensures + index < input@.len() ==> o == input@[index as int], +{ + match input.get(index) { + Some(&b) => b, + None => 0, + } +} + +/// The spec view of a value and the next position. +pub open spec fn view_bool(r: Option<(bool, usize)>) -> Option<(bool, nat)> { + match r { + Some((v, n)) => Some((v, n as nat)), + None => None, + } +} + +/// The spec view of a sign, a range and the next position. +pub open spec fn view_integer(r: Option<(bool, usize, usize, usize)>) -> Option<(bool, nat, nat, nat)> { + match r { + Some((v, a, b, n)) => Some((v, a as nat, b as nat, n as nat)), + None => None, + } +} + +/// The spec view of an unused-bits count, a range and the next position. +pub open spec fn view_bit_string(r: Option<(u8, usize, usize, usize)>) -> Option<(u8, nat, nat, nat)> { + match r { + Some((v, a, b, n)) => Some((v, a as nat, b as nat, n as nat)), + None => None, + } +} + +/// The spec view of a position. +pub open spec fn view_pos(r: Option) -> Option { + match r { + Some(n) => Some(n as nat), + None => None, + } +} + +/// The spec view of an optional component's extent. +pub open spec fn view_optional(r: Option>) -> Option> { + match r { + Some(Some((c, a, b))) => Some(Some((c, a as nat, b as nat))), + Some(None) => Some(None), + None => None, + } +} + +/// The spec view of an optional explicitly tagged component's range. +pub open spec fn view_optional_range(r: Option>) -> Option> { + match r { + Some(Some((a, b))) => Some(Some((a as nat, b as nat))), + Some(None) => Some(None), + None => None, + } +} + +/// §8.2, §11.1: a BOOLEAN at `pos`: its value and the next position. +pub fn read_boolean(input: &[u8], pos: usize, end: usize) -> (r: Option<(bool, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + super::contract::read_boolean_ensures(input@, pos as nat, end as nat, view_bool(r)), +{ + let t = read_tlv(input, pos, end)?; + // 8.2.1: primitive. + if !is_universal(t.tag, false, BOOLEAN) { + return None; + } + // cstart <= cend, so this is the number of contents octets exactly. + if t.cend.saturating_sub(t.cstart) != 1 { + return None; + } + let o = octet_at(input, t.cstart); + proof { + let c = input@.subrange(t.cstart as int, t.cend as int); + assert(c[0] == input@[t.cstart as int]); + } + if o == 0 { + Some((false, t.cend)) + } else if o == 255 { + Some((true, t.cend)) + } else { + None + } +} + +/// §8.3: an INTEGER at `pos`: whether it is negative, the range of its +/// magnitude octets, and the next position. +pub fn read_integer(input: &[u8], pos: usize, end: usize) -> (r: Option<(bool, usize, usize, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + super::contract::read_integer_ensures(input@, pos as nat, end as nat, view_integer(r)), +{ + let t = read_tlv(input, pos, end)?; + if !is_universal(t.tag, false, INTEGER) { + return None; + } + let len = t.cend.saturating_sub(t.cstart); + // 8.3.1: one or more octets. + if len == 0 { + return None; + } + let first = octet_at(input, t.cstart); + proof { + let c = input@.subrange(t.cstart as int, t.cend as int); + assert(c[0] == input@[t.cstart as int]); + } + // Never saturates: cstart < cend <= input.len(). + let second_index = t.cstart.saturating_add(1); + if len >= 2 { + let second = octet_at(input, second_index); + proof { + let c = input@.subrange(t.cstart as int, t.cend as int); + assert(c[1] == input@[second_index as int]); + } + // 8.3.2. + if (first == 255 && second >= 128) || (first == 0 && second < 128) { + return None; + } + } + let negative = first >= 128; + let start = if len >= 2 && first == 0 { + second_index + } else { + t.cstart + }; + Some((negative, start, t.cend, t.cend)) +} + +/// 2 to the power of a number of unused bits. +fn pow2_unused_exec(u: u8) -> (p: u32) + ensures + p as nat == pow2_unused(u), +{ + if u == 0 { + 1 + } else if u == 1 { + 2 + } else if u == 2 { + 4 + } else if u == 3 { + 8 + } else if u == 4 { + 16 + } else if u == 5 { + 32 + } else if u == 6 { + 64 + } else { + 128 + } +} + +/// §8.6, §11.2: a BIT STRING at `pos`: the number of unused bits, the range +/// of the octets that hold the bits, and the next position. +pub fn read_bit_string(input: &[u8], pos: usize, end: usize) -> (r: Option<(u8, usize, usize, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + super::contract::read_bit_string_ensures(input@, pos as nat, end as nat, view_bit_string(r)), +{ + let t = read_tlv(input, pos, end)?; + if !is_universal(t.tag, false, BIT_STRING) { + return None; + } + let len = t.cend.saturating_sub(t.cstart); + if len == 0 { + return None; + } + let unused = octet_at(input, t.cstart); + proof { + let c = input@.subrange(t.cstart as int, t.cend as int); + assert(c[0] == input@[t.cstart as int]); + } + // 8.6.2.2. + if unused > 7 { + return None; + } + if len == 1 { + // 8.6.2.3. + if unused != 0 { + return None; + } + } else { + // 11.2.1. Never saturates: cstart < cend. + let last_index = t.cend.saturating_sub(1); + let last = octet_at(input, last_index); + proof { + let c = input@.subrange(t.cstart as int, t.cend as int); + assert(c.last() == input@[last_index as int]); + } + let p = pow2_unused_exec(unused); + if !(last as u32).is_multiple_of(p) { + return None; + } + } + // Never saturates: cstart < cend. + Some((unused, t.cstart.saturating_add(1), t.cend, t.cend)) +} + +/// §8.7, §10.2: a primitive value of the universal type `number` taken as +/// octets: the contents range. +pub fn read_octets(input: &[u8], pos: usize, end: usize, number: u32) -> (r: Option<(usize, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + super::contract::read_octets_ensures(input@, pos as nat, end as nat, number, view_range(r)), +{ + let t = read_tlv(input, pos, end)?; + if is_universal(t.tag, false, number) { + Some((t.cstart, t.cend)) + } else { + None + } +} + +/// An OCTET STRING (§8.7). +pub fn read_octet_string(input: &[u8], pos: usize, end: usize) -> (r: Option<(usize, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + view_range(r) == super::spec::read_octet_string(input@, pos as nat, end as nat), +{ + read_octets(input, pos, end, OCTET_STRING) +} + +/// A UTF8String (its octets; the character rules are X.509's). +pub fn read_utf8_string(input: &[u8], pos: usize, end: usize) -> (r: Option<(usize, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + view_range(r) == super::spec::read_utf8_string(input@, pos as nat, end as nat), +{ + read_octets(input, pos, end, UTF8_STRING) +} + +/// A PrintableString (its octets). +pub fn read_printable_string(input: &[u8], pos: usize, end: usize) -> (r: Option<(usize, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + view_range(r) == super::spec::read_printable_string(input@, pos as nat, end as nat), +{ + read_octets(input, pos, end, PRINTABLE_STRING) +} + +/// An IA5String (its octets). +pub fn read_ia5_string(input: &[u8], pos: usize, end: usize) -> (r: Option<(usize, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + view_range(r) == super::spec::read_ia5_string(input@, pos as nat, end as nat), +{ + read_octets(input, pos, end, IA5_STRING) +} + +/// A UTCTime (its octets). +pub fn read_utc_time(input: &[u8], pos: usize, end: usize) -> (r: Option<(usize, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + view_range(r) == super::spec::read_utc_time(input@, pos as nat, end as nat), +{ + read_octets(input, pos, end, UTC_TIME) +} + +/// A GeneralizedTime (its octets). +pub fn read_generalized_time(input: &[u8], pos: usize, end: usize) -> (r: Option<(usize, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + view_range(r) == super::spec::read_generalized_time(input@, pos as nat, end as nat), +{ + read_octets(input, pos, end, GENERALIZED_TIME) +} + +/// §8.8: a NULL at `pos`: the next position. +pub fn read_null(input: &[u8], pos: usize, end: usize) -> (r: Option) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + super::contract::read_null_ensures(input@, pos as nat, end as nat, view_pos(r)), +{ + let t = read_tlv(input, pos, end)?; + if is_universal(t.tag, false, NULL) && t.cstart == t.cend { + Some(t.cend) + } else { + None + } +} + +/// §8.9, §8.11: a value whose tag is the universal constructed type +/// `number`: its contents range. +pub fn expect_constructed(input: &[u8], pos: usize, end: usize, number: u32) -> (r: Option<(usize, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + super::contract::expect_constructed_ensures(input@, pos as nat, end as nat, number, view_range(r)), +{ + let t = read_tlv(input, pos, end)?; + if is_universal(t.tag, true, number) { + Some((t.cstart, t.cend)) + } else { + None + } +} + +/// A SEQUENCE or SEQUENCE OF (§8.9, §8.10): its contents range. +pub fn expect_sequence(input: &[u8], pos: usize, end: usize) -> (r: Option<(usize, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + view_range(r) == super::spec::expect_sequence(input@, pos as nat, end as nat), +{ + expect_constructed(input, pos, end, SEQUENCE) +} + +/// A SET or SET OF (§8.11, §8.12): its contents range. +pub fn expect_set(input: &[u8], pos: usize, end: usize) -> (r: Option<(usize, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + view_range(r) == super::spec::expect_set(input@, pos as nat, end as nat), +{ + expect_constructed(input, pos, end, SET) +} + +/// An OPTIONAL component with the context-specific tag `[number]` (§8.14). +pub fn read_optional_context(input: &[u8], pos: usize, end: usize, number: u32) -> (r: Option< + Option<(bool, usize, usize)>, +>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + super::contract::read_optional_context_ensures(input@, pos as nat, end as nat, number, view_optional(r)), +{ + if pos >= end { + return Some(None); + } + let (tag, _) = read_identifier(input, pos, end)?; + match tag.class { + Class::ContextSpecific => { + if tag.number == number { + let t = read_tlv(input, pos, end)?; + Some(Some((tag.constructed, t.cstart, t.cend))) + } else { + Some(None) + } + }, + _ => Some(None), + } +} + +/// As [`read_optional_context`] for an EXPLICIT tag (§8.14.3): the inner +/// range, which must be constructed. +pub fn read_optional_explicit(input: &[u8], pos: usize, end: usize, number: u32) -> (r: Option>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + super::contract::read_optional_explicit_ensures(input@, pos as nat, end as nat, number, view_optional_range(r)), +{ + match read_optional_context(input, pos, end, number)? { + None => Some(None), + Some((constructed, cstart, cend)) => { + if constructed { + Some(Some((cstart, cend))) + } else { + None + } + }, + } +} + + +/// The components of an object identifier as spec numbers. +pub open spec fn nats(s: Seq) -> Seq { + s.map_values(|x: u64| x as nat) +} + +/// An optional list with a prefix put in front of it. +pub open spec fn prepend(p: Seq, o: Option>) -> Option> { + match o { + Some(r) => Some(p + r), + None => None, + } +} + +/// The spec view of the components and the next position. +pub open spec fn view_oid(r: Option<(Vec, usize)>) -> Option<(Seq, nat)> { + match r { + Some((v, n)) => Some((nats(v@), n as nat)), + None => None, + } +} + +/// The number of components, when there are any. +pub open spec fn oid_len(r: Option<(Vec, usize)>) -> Option { + match r { + Some((v, _)) => Some(v@.len()), + None => None, + } +} + +/// §8.19.2: the subidentifiers of `c`, one `u64` each. Pushes only values it +/// has read: no more than there are octets. +fn subidentifiers_exec(c: &[u8]) -> (r: Option>) + ensures + match r { + Some(v) => subidentifiers(c@) == Some(nats(v@)) && v@.len() <= c@.len(), + None => subidentifiers(c@) is None, + }, +{ + let mut subs: Vec = Vec::new(); + let mut i: usize = 0; + proof { + assert(c@.skip(0) =~= c@); + assert(nats(subs@) =~= Seq::::empty()); + match subidentifiers(c@) { + Some(r) => assert(Seq::::empty() + r =~= r), + None => {}, + } + } + while i < c.len() + invariant + i <= c.len(), + subs@.len() <= i, + subidentifiers(c@) == prepend(nats(subs@), subidentifiers(c@.skip(i as int))), + decreases c.len() - i, + { + proof { + super::lemmas::lemma_subidentifiers_step(c@, i as nat); + super::lemmas::lemma_series_end_in_range(c@, i as nat, c.len() as nat); + } + // 8.19.2: the leading octet of a subidentifier is not 80. + if octet_at(c, i) == 128 { + return None; + } + let (v, j) = read_series(c, i, c.len(), u64::MAX)?; + let ghost old_subs = subs@; + subs.push(v); + proof { + // The subidentifiers from `i` are `v` and then those from `j + 1`. + let rest = c@.skip(j as int + 1); + match subidentifiers(rest) { + Some(r) => { + assert(nats(subs@) =~= nats(old_subs).push(v as nat)); + assert(nats(old_subs) + (seq![v as nat] + r) =~= nats(subs@) + r); + }, + None => {}, + } + } + // Never saturates: j < c.len(). + i = j.saturating_add(1); + } + proof { + assert(c@.skip(i as int).len() == 0); + super::lemmas::lemma_subidentifiers_empty(c@.skip(i as int)); + assert(nats(subs@) + Seq::::empty() =~= nats(subs@)); + } + Some(subs) +} + +/// §8.19: an OBJECT IDENTIFIER at `pos`: its components and the next +/// position. The vector holds one component per subidentifier, plus one: +/// never more than the contents octets plus one. +pub fn read_oid(input: &[u8], pos: usize, end: usize) -> (r: Option<(Vec, usize)>) + requires + super::contract::read_requires(input@.len(), end as nat), + ensures + super::contract::read_oid_ensures(input@, pos as nat, end as nat, view_oid(r)), + super::contract::read_oid_alloc_ensures(input@, pos as nat, end as nat, oid_len(r)), +{ + let t = read_tlv(input, pos, end)?; + // 8.19.1: primitive. + if !is_universal(t.tag, false, OBJECT_IDENTIFIER) { + return None; + } + let c = vstd::slice::slice_subrange(input, t.cstart, t.cend); + let subs = subidentifiers_exec(c)?; + // 8.19.3: at least one subidentifier. + let first = match subs.first() { + Some(&f) => f, + None => return None, + }; + // 8.19.4: the first subidentifier is (X * 40) + Y. + let (x, y): (u64, u64) = if first < 40 { + (0, first) + } else if first < 80 { + (1, first.saturating_sub(40)) + } else { + (2, first.saturating_sub(80)) + }; + let mut arcs: Vec = Vec::new(); + arcs.push(x); + arcs.push(y); + let mut k: usize = 1; + proof { + assert(nats(arcs@) =~= seq![x as nat, y as nat]); + assert(nats(subs@).subrange(1, 1) =~= Seq::::empty()); + } + // 8.19.5: the others are the remaining components. + while k < subs.len() + invariant + 1 <= k <= subs.len(), + nats(arcs@) =~= seq![x as nat, y as nat] + nats(subs@).subrange(1, k as int), + arcs@.len() == k + 1, + decreases subs.len() - k, + { + let s = match subs.get(k) { + Some(&s) => s, + None => return None, + }; + let ghost before = nats(arcs@); + arcs.push(s); + proof { + assert(nats(arcs@) =~= before.push(s as nat)); + assert(nats(subs@).subrange(1, k as int + 1) =~= nats(subs@).subrange(1, k as int).push(s as nat)); + assert(seq![x as nat, y as nat] + nats(subs@).subrange(1, k as int + 1) =~= (seq![x as nat, y as nat] + nats( + subs@, + ).subrange(1, k as int)).push(s as nat)); + } + // Never saturates: k < subs.len(). + k = k.saturating_add(1); + } + proof { + assert(nats(subs@).subrange(1, subs.len() as int) =~= nats(subs@).skip(1)); + } + Some((arcs, t.cend)) +} + +} // verus! diff --git a/crates/clazel-der/src/encode.rs b/crates/clazel-der/src/encode.rs new file mode 100644 index 0000000..fb2ea72 --- /dev/null +++ b/crates/clazel-der/src/encode.rs @@ -0,0 +1,426 @@ +//! The executable encoder, proven to meet [`super::contract`]. +//! +//! It writes the DER encoding of a value (clauses 8, 10 and 11): the +//! identifier octets, the length in the fewest octets, and the contents. +//! Every function follows the spec function of the same name in +//! [`super::spec`]. None reserves memory ahead of the bytes it writes. + +#[allow(unused_imports)] +use super::spec::*; +#[allow(unused_imports)] +use vstd::prelude::*; + +verus! { + +/// Append the `width` octets spelling `value` base 256, most significant +/// first ([`be_bytes`]'s recursion). +pub(crate) fn push_be(out: &mut Vec, value: u64, width: usize) + requires + width <= 8, + (value as nat) < pow256(width as nat), + ensures + final(out)@ =~= old(out)@ + be_bytes(value as nat, width as nat), + decreases width, +{ + if width == 0 { + assert(be_bytes(value as nat, 0) =~= Seq::::empty()); + return; + } + // Never saturates: width >= 1. + let before = width.saturating_sub(1); + assert(pow256(width as nat) == 256 * pow256(before as nat)); + push_be(out, value / 256, before); + out.push((value % 256) as u8); + assert(out@ =~= old(out)@ + be_bytes(value as nat, width as nat)); +} + +/// Table 1: the value of bits 8 and 7 for a class. +fn class_bits_exec(class: Class) -> (b: u8) + ensures + b == class_bits(class), +{ + match class { + Class::Universal => 0, + Class::Application => 1, + Class::ContextSpecific => 2, + Class::Private => 3, + } +} + +/// §8.1.2: append the identifier octets of `tag`. +pub(crate) fn push_identifier(out: &mut Vec, tag: Tag) + requires + tag.number <= MAX_TAG_NUMBER, + ensures + final(out)@ =~= old(out)@ + encode_identifier(tag), +{ + // class_bits <= 3 and the constructed bit is 32, so this is at most + // 3 * 64 + 32 = 224 and never saturates. + let first = class_bits_exec(tag.class).saturating_mul(64).saturating_add(if tag.constructed { + 32 + } else { + 0 + }); + if tag.number < 31 { + // 8.1.2.2. number < 31, so first + number <= 254. + out.push(first.saturating_add(tag.number as u8)); + } else if tag.number < 128 { + // 8.1.2.4, one subsequent octet. first + 31 <= 255. + out.push(first.saturating_add(31)); + out.push(tag.number as u8); + } else { + // 8.1.2.4, two subsequent octets: number <= 16383, so + // number / 128 <= 127 and 128 + number / 128 <= 255. + out.push(first.saturating_add(31)); + out.push(128u32.saturating_add(tag.number / 128) as u8); + out.push((tag.number % 128) as u8); + } + assert(out@ =~= old(out)@ + encode_identifier(tag)); +} + +/// §8.1.3, §10.1: append the length octets of `len`: the short form for 127 +/// or less, otherwise the long form in the fewest octets. +pub(crate) fn push_length(out: &mut Vec, len: usize) + ensures + final(out)@ =~= old(out)@ + super::spec::encode_length(len as nat), +{ + proof { + super::lemmas::lemma_pow256_8(); + } + if len < 128 { + // 8.1.3.4. + out.push(len as u8); + return; + } + // 8.1.3.5. `len` is at most 2^64 - 1 (usize is at most 64 bits). + let l = len as u64; + let w: usize = if l < 0x100 { + 1 + } else if l < 0x1_0000 { + 2 + } else if l < 0x100_0000 { + 3 + } else if l < 0x1_0000_0000 { + 4 + } else if l < 0x100_0000_0000 { + 5 + } else if l < 0x1_0000_0000_0000 { + 6 + } else if l < 0x100_0000_0000_0000 { + 7 + } else { + 8 + }; + assert(w as nat == length_octets(len as nat)); + // w <= 8, so 128 + w <= 136. + out.push(128u8.saturating_add(w as u8)); + push_be(out, l, w); + assert(out@ =~= old(out)@ + super::spec::encode_length(len as nat)); +} + +/// §8.1.3, §10.1: the length octets of `len`, in the shortest form. +pub fn encode_length(len: usize) -> (out: Vec) + ensures + super::contract::encode_length_ensures(len as nat, out@), +{ + let mut out: Vec = Vec::new(); + push_length(&mut out, len); + assert(out@ =~= Seq::::empty() + super::spec::encode_length(len as nat)); + out +} + +/// §8.1.1.1: the encoding of a value with the given tag and contents: +/// identifier octets, length octets, contents octets. The output is the +/// contents and at most 12 octets more. +pub fn encode_tlv(tag: Tag, contents: &[u8]) -> (out: Vec) + requires + super::contract::encode_tlv_requires(tag), + ensures + super::contract::encode_tlv_ensures(tag, contents@, out@), + super::contract::encode_alloc_ensures(contents@.len(), out@.len()), +{ + // A slice is shorter than 2^64 octets: usize is at most 64 bits. + let _len64 = contents.len() as u64; + proof { + super::lemmas::lemma_pow256_8(); + assert(_len64 as nat == contents@.len()); + assert(tlv_ok(tag, contents@)); + } + let mut out: Vec = Vec::new(); + push_identifier(&mut out, tag); + push_length(&mut out, contents.len()); + out.extend_from_slice(contents); + proof { + super::lemmas::lemma_encode_tlv_len(tag, contents@); + assert(out@ =~= super::spec::encode_tlv(tag, contents@)); + } + out +} + +/// §8.2.2, §11.1: a BOOLEAN. +pub fn encode_boolean(v: bool) -> (out: Vec) + ensures + out@ =~= super::spec::encode_boolean(v), +{ + let contents: [u8; 1] = [if v { + 0xFF + } else { + 0x00 + }]; + proof { + assert(contents@ =~= seq![if v { 0xFFu8 } else { 0x00u8 }]); + } + encode_tlv(Tag { class: Class::Universal, constructed: false, number: BOOLEAN }, &contents) +} + +/// §8.7.2: a primitive OCTET STRING. +pub fn encode_octet_string(s: &[u8]) -> (out: Vec) + ensures + out@ =~= super::spec::encode_octet_string(s@), + super::contract::encode_alloc_ensures(s@.len(), out@.len()), +{ + encode_tlv(Tag { class: Class::Universal, constructed: false, number: OCTET_STRING }, s) +} + +/// A primitive value of the universal type `number` (UTF8String, +/// PrintableString, IA5String, UTCTime, GeneralizedTime). +pub fn encode_octets(number: u32, s: &[u8]) -> (out: Vec) + requires + number <= MAX_TAG_NUMBER, + ensures + out@ =~= super::spec::encode_octets(number, s@), + super::contract::encode_alloc_ensures(s@.len(), out@.len()), +{ + encode_tlv(Tag { class: Class::Universal, constructed: false, number }, s) +} + +/// §8.8: NULL, `05 00`. +pub fn encode_null() -> (out: Vec) + ensures + out@ =~= super::spec::encode_null(), +{ + let empty: [u8; 0] = []; + proof { + assert(empty@ =~= Seq::::empty()); + } + encode_tlv(Tag { class: Class::Universal, constructed: false, number: NULL }, &empty) +} + +/// §8.6.2: a primitive BIT STRING: `unused` is the number of unused bits (0 +/// to 7) of the last octet of `bits`, which are zero, and zero when `bits` +/// is empty. +pub fn encode_bit_string(unused: u8, bits: &[u8]) -> (out: Vec) + ensures + out@ =~= super::spec::encode_bit_string(unused, bits@), + super::contract::encode_alloc_ensures(bits@.len() + 1, out@.len()), +{ + let mut contents: Vec = Vec::new(); + contents.push(unused); + contents.extend_from_slice(bits); + proof { + assert(contents@ =~= seq![unused] + bits@); + } + encode_tlv(Tag { class: Class::Universal, constructed: false, number: BIT_STRING }, contents.as_slice()) +} + +/// §8.9.1: a SEQUENCE whose contents are the concatenated encodings of its +/// components. +pub fn encode_sequence(contents: &[u8]) -> (out: Vec) + ensures + out@ =~= super::spec::encode_sequence(contents@), + super::contract::encode_alloc_ensures(contents@.len(), out@.len()), +{ + encode_tlv(Tag { class: Class::Universal, constructed: true, number: SEQUENCE }, contents) +} + +/// §8.11.1: a SET whose contents are the concatenated encodings of its +/// components, in the order given. +pub fn encode_set(contents: &[u8]) -> (out: Vec) + ensures + out@ =~= super::spec::encode_set(contents@), + super::contract::encode_alloc_ensures(contents@.len(), out@.len()), +{ + encode_tlv(Tag { class: Class::Universal, constructed: true, number: SET }, contents) +} + +/// §8.14: a value with the context-specific tag `[number]` and the given +/// contents: for EXPLICIT tagging, `constructed` and the contents one +/// complete encoding; for IMPLICIT tagging, the base encoding's form and +/// contents. +pub fn encode_context(number: u32, constructed: bool, contents: &[u8]) -> (out: Vec) + requires + number <= MAX_TAG_NUMBER, + ensures + out@ =~= super::spec::encode_context(number, constructed, contents@), + super::contract::encode_alloc_ensures(contents@.len(), out@.len()), +{ + encode_tlv(Tag { class: Class::ContextSpecific, constructed, number }, contents) +} + +/// The contents octets of the INTEGER whose value is the number the octets +/// `m` spell (non-negative): leading zero octets removed, one zero octet for +/// zero, and a zero octet in front of a first octet with bit 8 set. +pub(crate) fn integer_unsigned_contents_exec(m: &[u8]) -> (c: Vec) + ensures + c@ =~= integer_unsigned_contents(m@), + c@.len() <= m@.len() + 1, +{ + let mut k: usize = 0; + proof { + assert(m@.skip(0) =~= m@); + } + while k < m.len() && super::decode::octet_at(m, k) == 0 + invariant + k <= m.len(), + strip_leading_zeros(m@) =~= strip_leading_zeros(m@.skip(k as int)), + decreases m.len() - k, + { + proof { + assert(m@.skip(k as int).drop_first() =~= m@.skip(k as int + 1)); + } + // Never saturates: k < m.len(). + k = k.saturating_add(1); + } + proof { + if k == m.len() { + assert(m@.skip(k as int) =~= Seq::::empty()); + } + } + let mut c: Vec = Vec::new(); + if k == m.len() { + c.push(0); + return c; + } + let first = match m.get(k) { + Some(&b) => b, + None => 0, + }; + if first >= 128 { + c.push(0); + } + let ghost before = c@; + c.extend_from_slice(vstd::slice::slice_subrange(m, k, m.len())); + proof { + assert(m@.skip(k as int) =~= m@.subrange(k as int, m@.len() as int)); + assert(strip_leading_zeros(m@.skip(k as int)) =~= m@.skip(k as int)); + } + c +} + +/// §8.3: the INTEGER whose value is the number the octets `magnitude` spell +/// (non-negative, most significant octet first, leading zeros allowed). The +/// output is at most 13 octets more than `magnitude`. +pub fn encode_integer_unsigned(magnitude: &[u8]) -> (out: Vec) + ensures + super::contract::encode_integer_unsigned_ensures(magnitude@, out@), + out@.len() <= magnitude@.len() + 13, +{ + let contents = integer_unsigned_contents_exec(magnitude); + encode_tlv(Tag { class: Class::Universal, constructed: false, number: INTEGER }, contents.as_slice()) +} + +/// §8.19.2: append the digits of `w` base 128, most significant first, each +/// with bit 8 set ([`subidentifier_prefix`]'s recursion; at most ten digits +/// for a `u64`). +fn push_prefix(out: &mut Vec, w: u64) + ensures + final(out)@ =~= old(out)@ + subidentifier_prefix(w as nat), + decreases w, +{ + if w == 0 { + assert(subidentifier_prefix(0) =~= Seq::::empty()); + return; + } + push_prefix(out, w / 128); + // w % 128 <= 127, so 128 + w % 128 <= 255. + out.push(128u8.saturating_add((w % 128) as u8)); + assert(out@ =~= old(out)@ + subidentifier_prefix(w as nat)); +} + +/// §8.19.2: append the subidentifier `v` in the fewest possible octets. +fn push_subidentifier(out: &mut Vec, v: u64) + ensures + final(out)@ =~= old(out)@ + encode_subidentifier(v as nat), +{ + push_prefix(out, v / 128); + out.push((v % 128) as u8); +} + +/// §8.19: an OBJECT IDENTIFIER with the given components. The first is 0, 1 +/// or 2; the second is below 40 unless the first is 2; `40 * first + second` +/// and every other component fit a `u64`. +pub fn encode_oid(arcs: &[u64]) -> (out: Vec) + requires + super::contract::encode_oid_requires(super::decode::nats(arcs@)), + ensures + super::contract::encode_oid_ensures(super::decode::nats(arcs@), out@), +{ + let a0 = match arcs.first() { + Some(&a) => a, + None => return Vec::new(), + }; + let a1 = match arcs.get(1) { + Some(&a) => a, + None => return Vec::new(), + }; + // oid_valid: 40 * a0 + a1 < 2^64, so neither operation saturates. + let first = a0.saturating_mul(40).saturating_add(a1); + let mut contents: Vec = Vec::new(); + push_subidentifier(&mut contents, first); + let ghost n = super::decode::nats(arcs@); + proof { + assert(n.subrange(2, 2) =~= Seq::::empty()); + assert(seq![first as nat] + n.subrange(2, 2) =~= seq![first as nat]); + super::lemmas::lemma_encode_subidentifiers_single(first as nat); + } + let mut k: usize = 2; + while k < arcs.len() + invariant + 2 <= k <= arcs.len(), + n == super::decode::nats(arcs@), + first as nat == 40 * n[0] + n[1], + contents@ =~= encode_subidentifiers(seq![first as nat] + n.subrange(2, k as int)), + decreases arcs.len() - k, + { + let a = match arcs.get(k) { + Some(&a) => a, + None => return Vec::new(), + }; + let ghost before = contents@; + push_subidentifier(&mut contents, a); + proof { + let prefix = seq![first as nat] + n.subrange(2, k as int); + super::lemmas::lemma_encode_subidentifiers_append(prefix, seq![a as nat]); + super::lemmas::lemma_encode_subidentifiers_single(a as nat); + assert(prefix + seq![a as nat] =~= seq![first as nat] + n.subrange(2, k as int + 1)); + } + // Never saturates: k < arcs.len(). + k = k.saturating_add(1); + } + proof { + assert(n.subrange(2, arcs@.len() as int) =~= n.skip(2)); + assert(contents@ =~= oid_contents(n)); + } + encode_tlv(Tag { class: Class::Universal, constructed: false, number: OBJECT_IDENTIFIER }, contents.as_slice()) +} + +/// The `DigestInfo` of RFC 8017 §9.2: `SEQUENCE { SEQUENCE { algorithm OID, +/// parameters NULL }, digest OCTET STRING }`. +pub fn encode_digest_info(arcs: &[u64], digest: &[u8]) -> (out: Vec) + requires + super::contract::encode_oid_requires(super::decode::nats(arcs@)), + ensures + out@ =~= super::spec::encode_digest_info(super::decode::nats(arcs@), digest@), +{ + let oid = encode_oid(arcs); + let null = encode_null(); + let mut algorithm = oid; + algorithm.extend_from_slice(null.as_slice()); + let algorithm = encode_sequence(algorithm.as_slice()); + let octets = encode_octet_string(digest); + let mut body = algorithm; + body.extend_from_slice(octets.as_slice()); + encode_sequence(body.as_slice()) +} + +} // verus! diff --git a/crates/clazel-der/src/examples.rs b/crates/clazel-der/src/examples.rs new file mode 100644 index 0000000..13c5ede --- /dev/null +++ b/crates/clazel-der/src/examples.rs @@ -0,0 +1,388 @@ +//! The standards' own examples, evaluated by Verus against the spec: the +//! examples of ITU-T X.690 (08/2015) §8.1.3.4, §8.1.3.5, §8.2.2, §8.6.4.2, +//! §8.8, §8.9.3, §8.14.4 and §8.19.5, and those of Kaliski's Layman's Guide +//! (§5.4, §5.7 Table 3, §5.8, §5.9, §5.10, §5.6, §5.17, §6.2.6), plus the +//! `DigestInfo` prefix of RFC 8017 §9.2 (Note 1, SHA-256). Each example is +//! checked both ways: the encoder writes the printed bytes, and the decoder +//! reads them back. The non-DER alternatives the standards print beside them +//! (a long-form length that fits the short form, a TRUE that is not `0xFF`, +//! indefinite lengths, constructed strings, padding bits that are not zero) +//! are checked to be rejected. +//! +//! This is a module of its own because Verus evaluates every `by +//! (compute_only)` assertion of a module it verifies, even with +//! `--verify-function`. A mutant that breaks an example would otherwise +//! fail every kill certificate placed next to it, not only the one about the +//! example. +//! +//! The `{2 999 3}` example is the one in this edition (§8.19.5); the 2002 +//! edition's `{2 100 3}` is added beside it, computed by hand: the first +//! subidentifier is 2 * 40 + 100 = 180 = 1 * 128 + 52, so `81 34`. + +#[allow(unused_imports)] +use super::spec::*; +#[allow(unused_imports)] +use vstd::prelude::*; + +verus! { + +/// "Smith" (§8.9.3), "Jones" (§8.14.4) and the other strings, as octets. +pub open spec fn smith() -> Seq { + seq![0x53u8, 0x6D, 0x69, 0x74, 0x68] +} + +/// "Jones". +pub open spec fn jones() -> Seq { + seq![0x4Au8, 0x6F, 0x6E, 0x65, 0x73] +} + +/// The tag `[APPLICATION 3]`, primitive. +pub open spec fn application_3() -> Tag { + Tag { class: Class::Application, constructed: false, number: 3 } +} + +/// The tag `[APPLICATION 7]`, constructed. +pub open spec fn application_7() -> Tag { + Tag { class: Class::Application, constructed: true, number: 7 } +} + +/// The X.690 examples: the length octets, BOOLEAN, BIT STRING, NULL, +/// SEQUENCE, the tagged-type chain of §8.14.4, and the OBJECT IDENTIFIER. +pub proof fn lemma_examples_x690() + ensures + // 8.1.3.4: "L = 38 can be encoded as 00100110". + encode_length(38) == seq![0x26u8], + read_length(seq![0x26u8], 0, 1) == Some((38nat, 1nat)), + // 8.1.3.5: "L = 201 can be encoded as: 10000001 11001001". + encode_length(201) == seq![0x81u8, 0xC9], + read_length(seq![0x81u8, 0xC9], 0, 2) == Some((201nat, 2nat)), + // DER (10.1): 38 in the long form 81 26 is not the minimum. + read_length(seq![0x81u8, 0x26], 0, 2) is None, + // The indefinite form 80 (8.1.3.6) and the reserved FF (8.1.3.5 c). + read_length(seq![0x80u8], 0, 1) is None, + read_length(seq![0xFFu8], 0, 1) is None, + // A leading zero length octet (10.1). + read_length(seq![0x82u8, 0x00, 0xC9], 0, 3) is None, + // 8.2.2: "If of type BOOLEAN, the value TRUE can be encoded as: 01 01 FF". + encode_boolean(true) == seq![0x01u8, 0x01, 0xFF], + read_boolean(seq![0x01u8, 0x01, 0xFF], 0, 3) == Some((true, 3nat)), + encode_boolean(false) == seq![0x01u8, 0x01, 0x00], + // 8.2.2 allows any non-zero octet for TRUE; 11.1 (DER) requires FF. + read_boolean(seq![0x01u8, 0x01, 0x01], 0, 3) is None, + // 8.6.4.2: the value '0A3B5F291CD'H, primitive: 03 07 04 0A3B5F291CD0. + encode_bit_string(4, seq![0x0Au8, 0x3B, 0x5F, 0x29, 0x1C, 0xD0]) == seq![ + 0x03u8, 0x07, 0x04, 0x0A, 0x3B, 0x5F, 0x29, 0x1C, 0xD0, + ], + read_bit_string(seq![0x03u8, 0x07, 0x04, 0x0A, 0x3B, 0x5F, 0x29, 0x1C, 0xD0], 0, 9) == Some( + (4u8, 3nat, 9nat, 9nat), + ), + // 8.6.4.2: the same value as a constructor (23 80 ...) is not DER. + read_tlv(seq![0x23u8, 0x80, 0x03, 0x03, 0x00, 0x0A, 0x3B, 0x00, 0x00], 0, 9) is None, + // 11.2.1: unused bits that are not zero (the last octet 0xD1). + read_bit_string(seq![0x03u8, 0x07, 0x04, 0x0A, 0x3B, 0x5F, 0x29, 0x1C, 0xD1], 0, 9) is None, + // 8.6.2.2: an unused-bits octet above 7. + read_bit_string(seq![0x03u8, 0x02, 0x08, 0x00], 0, 4) is None, + // 8.6.2.3: an empty bit string is `03 01 00`, with no unused bits. + encode_bit_string(0, seq![]) == seq![0x03u8, 0x01, 0x00], + read_bit_string(seq![0x03u8, 0x01, 0x00], 0, 3) == Some((0u8, 3nat, 3nat, 3nat)), + read_bit_string(seq![0x03u8, 0x01, 0x01], 0, 3) is None, + // 8.8: "05 00". + encode_null() == seq![0x05u8, 0x00], + read_null(seq![0x05u8, 0x00], 0, 2) == Some(2nat), + // 8.8.2: a NULL with contents. + read_null(seq![0x05u8, 0x01, 0x00], 0, 3) is None, + // 8.9.3: SEQUENCE {name IA5String, ok BOOLEAN} {name "Smith", ok TRUE}: + // 30 0A 16 05 "Smith" 01 01 FF. + encode_sequence(encode_octets(IA5_STRING, smith()) + encode_boolean(true)) == seq![ + 0x30u8, 0x0A, 0x16, 0x05, 0x53, 0x6D, 0x69, 0x74, 0x68, 0x01, 0x01, 0xFF, + ], + expect_sequence(seq![0x30u8, 0x0A, 0x16, 0x05, 0x53, 0x6D, 0x69, 0x74, 0x68, 0x01, 0x01, 0xFF], 0, 12) == Some( + (2nat, 12nat), + ), + read_ia5_string(seq![0x30u8, 0x0A, 0x16, 0x05, 0x53, 0x6D, 0x69, 0x74, 0x68, 0x01, 0x01, 0xFF], 2, 12) == Some( + (4nat, 9nat), + ), + // 8.14.4, Type2 ::= [APPLICATION 3] IMPLICIT VisibleString: 43 05 "Jones". + encode_tlv(application_3(), jones()) == seq![0x43u8, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73], + read_tlv(seq![0x43u8, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73], 0, 7) == Some( + Tlv { tag: application_3(), cstart: 2, cend: 7 }, + ), + // Type3 ::= [2] Type2 (explicit): A2 07 43 05 "Jones". + encode_context(2, true, encode_tlv(application_3(), jones())) == seq![ + 0xA2u8, 0x07, 0x43, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73, + ], + read_optional_explicit(seq![0xA2u8, 0x07, 0x43, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73], 0, 9, 2) == Some( + Some((2nat, 9nat)), + ), + // Type4 ::= [APPLICATION 7] IMPLICIT Type3: 67 07 43 05 "Jones". + encode_tlv(application_7(), encode_tlv(application_3(), jones())) == seq![ + 0x67u8, 0x07, 0x43, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73, + ], + // Type5 ::= [2] IMPLICIT Type2: 82 05 "Jones". + encode_context(2, false, jones()) == seq![0x82u8, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73], + read_optional_context(seq![0x82u8, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73], 0, 7, 2) == Some( + Some((false, 2nat, 7nat)), + ), + // The component is absent when the next tag is another one, and when + // nothing is left. + read_optional_context(seq![0x82u8, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73], 0, 7, 3) == Some(None::<(bool, nat, nat)>), + read_optional_context(seq![0x82u8, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73], 7, 7, 2) == Some(None::<(bool, nat, nat)>), + // 8.19.5: {joint-iso-itu-t 999 3}: 06 03 88 37 03. + encode_oid(seq![2nat, 999, 3]) == seq![0x06u8, 0x03, 0x88, 0x37, 0x03], + read_oid(seq![0x06u8, 0x03, 0x88, 0x37, 0x03], 0, 5) == Some((seq![2nat, 999, 3], 5nat)), + // The 2002 edition's {2 100 3}: first subidentifier 180 = 81 34. + encode_oid(seq![2nat, 100, 3]) == seq![0x06u8, 0x03, 0x81, 0x34, 0x03], + read_oid(seq![0x06u8, 0x03, 0x81, 0x34, 0x03], 0, 5) == Some((seq![2nat, 100, 3], 5nat)), + // 8.19.2: a subidentifier padded with 80 is not the fewest octets. + read_oid(seq![0x06u8, 0x04, 0x80, 0x81, 0x34, 0x03], 0, 6) is None, + // A subidentifier whose last octet has bit 8 set never ends. + read_oid(seq![0x06u8, 0x02, 0x88, 0xB7], 0, 4) is None, +{ + assert(encode_length(38) =~= seq![0x26u8]) by (compute_only); + assert(read_length(seq![0x26u8], 0, 1) == Some((38nat, 1nat))) by (compute_only); + assert(encode_length(201) =~= seq![0x81u8, 0xC9]) by (compute_only); + assert(read_length(seq![0x81u8, 0xC9], 0, 2) == Some((201nat, 2nat))) by (compute_only); + assert(read_length(seq![0x81u8, 0x26], 0, 2) is None) by (compute_only); + assert(read_length(seq![0x80u8], 0, 1) is None) by (compute_only); + assert(read_length(seq![0xFFu8], 0, 1) is None) by (compute_only); + assert(read_length(seq![0x82u8, 0x00, 0xC9], 0, 3) is None) by (compute_only); + assert(encode_boolean(true) =~= seq![0x01u8, 0x01, 0xFF]) by (compute_only); + assert(read_boolean(seq![0x01u8, 0x01, 0xFF], 0, 3) == Some((true, 3nat))) by (compute_only); + assert(encode_boolean(false) =~= seq![0x01u8, 0x01, 0x00]) by (compute_only); + assert(read_boolean(seq![0x01u8, 0x01, 0x01], 0, 3) is None) by (compute_only); + assert(encode_bit_string(4, seq![0x0Au8, 0x3B, 0x5F, 0x29, 0x1C, 0xD0]) =~= seq![ + 0x03u8, 0x07, 0x04, 0x0A, 0x3B, 0x5F, 0x29, 0x1C, 0xD0, + ]) by (compute_only); + assert(read_bit_string(seq![0x03u8, 0x07, 0x04, 0x0A, 0x3B, 0x5F, 0x29, 0x1C, 0xD0], 0, 9) == Some( + (4u8, 3nat, 9nat, 9nat), + )) by (compute_only); + assert(read_tlv(seq![0x23u8, 0x80, 0x03, 0x03, 0x00, 0x0A, 0x3B, 0x00, 0x00], 0, 9) is None) by (compute_only); + assert(read_bit_string(seq![0x03u8, 0x07, 0x04, 0x0A, 0x3B, 0x5F, 0x29, 0x1C, 0xD1], 0, 9) is None) by (compute_only); + assert(read_bit_string(seq![0x03u8, 0x02, 0x08, 0x00], 0, 4) is None) by (compute_only); + assert(encode_bit_string(0, seq![]) =~= seq![0x03u8, 0x01, 0x00]) by (compute_only); + assert(read_bit_string(seq![0x03u8, 0x01, 0x00], 0, 3) == Some((0u8, 3nat, 3nat, 3nat))) by (compute_only); + assert(read_bit_string(seq![0x03u8, 0x01, 0x01], 0, 3) is None) by (compute_only); + assert(encode_null() =~= seq![0x05u8, 0x00]) by (compute_only); + assert(read_null(seq![0x05u8, 0x00], 0, 2) == Some(2nat)) by (compute_only); + assert(read_null(seq![0x05u8, 0x01, 0x00], 0, 3) is None) by (compute_only); + assert(encode_sequence(encode_octets(IA5_STRING, smith()) + encode_boolean(true)) =~= seq![ + 0x30u8, 0x0A, 0x16, 0x05, 0x53, 0x6D, 0x69, 0x74, 0x68, 0x01, 0x01, 0xFF, + ]) by (compute_only); + assert(expect_sequence(seq![0x30u8, 0x0A, 0x16, 0x05, 0x53, 0x6D, 0x69, 0x74, 0x68, 0x01, 0x01, 0xFF], 0, 12) == Some( + (2nat, 12nat), + )) by (compute_only); + assert(read_ia5_string(seq![0x30u8, 0x0A, 0x16, 0x05, 0x53, 0x6D, 0x69, 0x74, 0x68, 0x01, 0x01, 0xFF], 2, 12) == Some( + (4nat, 9nat), + )) by (compute_only); + assert(encode_tlv(application_3(), jones()) =~= seq![0x43u8, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73]) by (compute_only); + assert(read_tlv(seq![0x43u8, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73], 0, 7) == Some( + Tlv { tag: application_3(), cstart: 2, cend: 7 }, + )) by (compute_only); + assert(encode_context(2, true, encode_tlv(application_3(), jones())) =~= seq![ + 0xA2u8, 0x07, 0x43, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73, + ]) by (compute_only); + assert(read_optional_explicit(seq![0xA2u8, 0x07, 0x43, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73], 0, 9, 2) == Some( + Some((2nat, 9nat)), + )) by (compute_only); + assert(encode_tlv(application_7(), encode_tlv(application_3(), jones())) =~= seq![ + 0x67u8, 0x07, 0x43, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73, + ]) by (compute_only); + assert(encode_context(2, false, jones()) =~= seq![0x82u8, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73]) by (compute_only); + assert(read_optional_context(seq![0x82u8, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73], 0, 7, 2) == Some( + Some((false, 2nat, 7nat)), + )) by (compute_only); + assert(read_optional_context(seq![0x82u8, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73], 0, 7, 3) == Some(None::<(bool, nat, nat)>)) by (compute_only); + assert(read_optional_context(seq![0x82u8, 0x05, 0x4A, 0x6F, 0x6E, 0x65, 0x73], 7, 7, 2) == Some(None::<(bool, nat, nat)>)) by (compute_only); + assert(encode_oid(seq![2nat, 999, 3]) =~= seq![0x06u8, 0x03, 0x88, 0x37, 0x03]) by (compute_only); + assert(read_oid(seq![0x06u8, 0x03, 0x88, 0x37, 0x03], 0, 5) == Some((seq![2nat, 999, 3], 5nat))) by (compute_only); + assert(encode_oid(seq![2nat, 100, 3]) =~= seq![0x06u8, 0x03, 0x81, 0x34, 0x03]) by (compute_only); + assert(read_oid(seq![0x06u8, 0x03, 0x81, 0x34, 0x03], 0, 5) == Some((seq![2nat, 100, 3], 5nat))) by (compute_only); + assert(read_oid(seq![0x06u8, 0x04, 0x80, 0x81, 0x34, 0x03], 0, 6) is None) by (compute_only); + assert(read_oid(seq![0x06u8, 0x02, 0x88, 0xB7], 0, 4) is None) by (compute_only); +} + +/// Kaliski's examples, with the `DigestInfo` prefix of RFC 8017. +pub proof fn lemma_examples_kaliski() + ensures + // §5.4: the DER encoding of the BIT STRING "011011100101110111" is + // 03 04 06 6e 5d c0 (padded with zero bits); the BER padding "e0", + // the long form of the length and the constructed encoding are not DER. + encode_bit_string(6, seq![0x6Eu8, 0x5D, 0xC0]) == seq![0x03u8, 0x04, 0x06, 0x6E, 0x5D, 0xC0], + read_bit_string(seq![0x03u8, 0x04, 0x06, 0x6E, 0x5D, 0xC0], 0, 6) == Some((6u8, 3nat, 6nat, 6nat)), + read_bit_string(seq![0x03u8, 0x04, 0x06, 0x6E, 0x5D, 0xE0], 0, 6) is None, + read_bit_string(seq![0x03u8, 0x81, 0x04, 0x06, 0x6E, 0x5D, 0xC0], 0, 7) is None, + read_bit_string(seq![0x23u8, 0x09, 0x03, 0x03, 0x00, 0x6E, 0x5D, 0x03, 0x02, 0x06, 0xC0], 0, 11) is None, + // §5.7 Table 3: INTEGER 0, 127, 128, 256, -128, -129. + encode_integer_unsigned(seq![]) == seq![0x02u8, 0x01, 0x00], + encode_integer_unsigned(seq![0x7Fu8]) == seq![0x02u8, 0x01, 0x7F], + encode_integer_unsigned(seq![0x80u8]) == seq![0x02u8, 0x02, 0x00, 0x80], + encode_integer_unsigned(seq![0x01u8, 0x00]) == seq![0x02u8, 0x02, 0x01, 0x00], + read_integer(seq![0x02u8, 0x01, 0x00], 0, 3) == Some((false, 2nat, 3nat, 3nat)), + read_integer(seq![0x02u8, 0x01, 0x7F], 0, 3) == Some((false, 2nat, 3nat, 3nat)), + read_integer(seq![0x02u8, 0x02, 0x00, 0x80], 0, 4) == Some((false, 3nat, 4nat, 4nat)), + read_integer(seq![0x02u8, 0x02, 0x01, 0x00], 0, 4) == Some((false, 2nat, 4nat, 4nat)), + read_integer(seq![0x02u8, 0x01, 0x80], 0, 3) == Some((true, 2nat, 3nat, 3nat)), + read_integer(seq![0x02u8, 0x02, 0xFF, 0x7F], 0, 4) == Some((true, 2nat, 4nat, 4nat)), + integer_value(seq![0x80u8]) == -128, + integer_value(seq![0xFFu8, 0x7F]) == -129, + integer_value(seq![0x00u8, 0x80]) == 128, + // Redundant leading octets (8.3.2): 00 7F, 00 00, FF FF, FF 80. + read_integer(seq![0x02u8, 0x02, 0x00, 0x7F], 0, 4) is None, + read_integer(seq![0x02u8, 0x02, 0x00, 0x00], 0, 4) is None, + read_integer(seq![0x02u8, 0x02, 0xFF, 0xFF], 0, 4) is None, + read_integer(seq![0x02u8, 0x02, 0xFF, 0x80], 0, 4) is None, + read_integer(seq![0x02u8, 0x00], 0, 2) is None, + // §5.8: NULL is 05 00 in DER, never 05 81 00. + read_null(seq![0x05u8, 0x81, 0x00], 0, 3) is None, + // §5.9: {1 2 840 113549}: 06 06 2a 86 48 86 f7 0d. + encode_oid(seq![1nat, 2, 840, 113549]) == seq![0x06u8, 0x06, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D], + read_oid(seq![0x06u8, 0x06, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D], 0, 8) == Some( + (seq![1nat, 2, 840, 113549], 8nat), + ), + // §5.10: the OCTET STRING 01 23 45 67 89 ab cd ef; the long length and + // the constructed encoding are not DER. + encode_octet_string(seq![0x01u8, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]) == seq![ + 0x04u8, 0x08, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + ], + read_octet_string(seq![0x04u8, 0x08, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF], 0, 10) == Some( + (2nat, 10nat), + ), + read_octet_string(seq![0x04u8, 0x81, 0x08, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF], 0, 11) is None, + read_octet_string( + seq![0x24u8, 0x0C, 0x04, 0x04, 0x01, 0x23, 0x45, 0x67, 0x04, 0x04, 0x89, 0xAB, 0xCD, 0xEF], + 0, + 14, + ) is None, + // §5.6: the IA5String "test1@rsa.com": 16 0d ... + encode_octets(IA5_STRING, seq![0x74u8, 0x65, 0x73, 0x74, 0x31, 0x40, 0x72, 0x73, 0x61, 0x2E, 0x63, 0x6F, 0x6D]) == seq![ + 0x16u8, 0x0D, 0x74, 0x65, 0x73, 0x74, 0x31, 0x40, 0x72, 0x73, 0x61, 0x2E, 0x63, 0x6F, 0x6D, + ], + // §5.17: the UTCTime "910506234540Z": 17 0d ... + encode_octets(UTC_TIME, seq![0x39u8, 0x31, 0x30, 0x35, 0x30, 0x36, 0x32, 0x33, 0x34, 0x35, 0x34, 0x30, 0x5A]) == seq![ + 0x17u8, 0x0D, 0x39, 0x31, 0x30, 0x35, 0x30, 0x36, 0x32, 0x33, 0x34, 0x35, 0x34, 0x30, 0x5A, + ], + // §6.2.6: the X.501 Name of the Guide, a SEQUENCE of three SETs of + // SEQUENCE { OID, PrintableString }: 30 42 31 0b 30 09 ... + encode_sequence( + encode_set(encode_sequence(encode_oid(seq![2nat, 5, 4, 6]) + encode_octets(PRINTABLE_STRING, seq![0x55u8, 0x53]))) + + encode_set( + encode_sequence( + encode_oid(seq![2nat, 5, 4, 10]) + encode_octets( + PRINTABLE_STRING, + seq![ + 0x45u8, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x20, 0x4F, 0x72, 0x67, 0x61, 0x6E, 0x69, 0x7A, 0x61, 0x74, + 0x69, 0x6F, 0x6E, + ], + ), + ), + ) + encode_set( + encode_sequence( + encode_oid(seq![2nat, 5, 4, 3]) + encode_octets( + PRINTABLE_STRING, + seq![0x54u8, 0x65, 0x73, 0x74, 0x20, 0x55, 0x73, 0x65, 0x72, 0x20, 0x31], + ), + ), + ), + ) == seq![ + 0x30u8, 0x42, 0x31, 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x1D, 0x30, 0x1B, 0x06, + 0x03, 0x55, 0x04, 0x0A, 0x13, 0x14, 0x45, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x20, 0x4F, 0x72, 0x67, 0x61, 0x6E, 0x69, + 0x7A, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x0B, 0x54, 0x65, 0x73, + 0x74, 0x20, 0x55, 0x73, 0x65, 0x72, 0x20, 0x31, + ], + // RFC 8017 §9.2 Note 1: the `DigestInfo` of SHA-256 is + // 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 || H. + encode_digest_info( + seq![2nat, 16, 840, 1, 101, 3, 4, 2, 1], + seq![ + 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, + ], + ) == seq![ + 0x30u8, 0x31, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20, 0, 1, + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + ], +{ + assert(encode_bit_string(6, seq![0x6Eu8, 0x5D, 0xC0]) =~= seq![0x03u8, 0x04, 0x06, 0x6E, 0x5D, 0xC0]) by (compute_only); + assert(read_bit_string(seq![0x03u8, 0x04, 0x06, 0x6E, 0x5D, 0xC0], 0, 6) == Some((6u8, 3nat, 6nat, 6nat))) by (compute_only); + assert(read_bit_string(seq![0x03u8, 0x04, 0x06, 0x6E, 0x5D, 0xE0], 0, 6) is None) by (compute_only); + assert(read_bit_string(seq![0x03u8, 0x81, 0x04, 0x06, 0x6E, 0x5D, 0xC0], 0, 7) is None) by (compute_only); + assert(read_bit_string(seq![0x23u8, 0x09, 0x03, 0x03, 0x00, 0x6E, 0x5D, 0x03, 0x02, 0x06, 0xC0], 0, 11) is None) by (compute_only); + assert(encode_integer_unsigned(seq![]) =~= seq![0x02u8, 0x01, 0x00]) by (compute_only); + assert(encode_integer_unsigned(seq![0x7Fu8]) =~= seq![0x02u8, 0x01, 0x7F]) by (compute_only); + assert(encode_integer_unsigned(seq![0x80u8]) =~= seq![0x02u8, 0x02, 0x00, 0x80]) by (compute_only); + assert(encode_integer_unsigned(seq![0x01u8, 0x00]) =~= seq![0x02u8, 0x02, 0x01, 0x00]) by (compute_only); + assert(read_integer(seq![0x02u8, 0x01, 0x00], 0, 3) == Some((false, 2nat, 3nat, 3nat))) by (compute_only); + assert(read_integer(seq![0x02u8, 0x01, 0x7F], 0, 3) == Some((false, 2nat, 3nat, 3nat))) by (compute_only); + assert(read_integer(seq![0x02u8, 0x02, 0x00, 0x80], 0, 4) == Some((false, 3nat, 4nat, 4nat))) by (compute_only); + assert(read_integer(seq![0x02u8, 0x02, 0x01, 0x00], 0, 4) == Some((false, 2nat, 4nat, 4nat))) by (compute_only); + assert(read_integer(seq![0x02u8, 0x01, 0x80], 0, 3) == Some((true, 2nat, 3nat, 3nat))) by (compute_only); + assert(read_integer(seq![0x02u8, 0x02, 0xFF, 0x7F], 0, 4) == Some((true, 2nat, 4nat, 4nat))) by (compute_only); + assert(integer_value(seq![0x80u8]) == -128) by (compute_only); + assert(integer_value(seq![0xFFu8, 0x7F]) == -129) by (compute_only); + assert(integer_value(seq![0x00u8, 0x80]) == 128) by (compute_only); + assert(read_integer(seq![0x02u8, 0x02, 0x00, 0x7F], 0, 4) is None) by (compute_only); + assert(read_integer(seq![0x02u8, 0x02, 0x00, 0x00], 0, 4) is None) by (compute_only); + assert(read_integer(seq![0x02u8, 0x02, 0xFF, 0xFF], 0, 4) is None) by (compute_only); + assert(read_integer(seq![0x02u8, 0x02, 0xFF, 0x80], 0, 4) is None) by (compute_only); + assert(read_integer(seq![0x02u8, 0x00], 0, 2) is None) by (compute_only); + assert(read_null(seq![0x05u8, 0x81, 0x00], 0, 3) is None) by (compute_only); + assert(encode_oid(seq![1nat, 2, 840, 113549]) =~= seq![0x06u8, 0x06, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D]) by (compute_only); + assert(read_oid(seq![0x06u8, 0x06, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D], 0, 8) == Some( + (seq![1nat, 2, 840, 113549], 8nat), + )) by (compute_only); + assert(encode_octet_string(seq![0x01u8, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]) =~= seq![ + 0x04u8, 0x08, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + ]) by (compute_only); + assert(read_octet_string(seq![0x04u8, 0x08, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF], 0, 10) == Some( + (2nat, 10nat), + )) by (compute_only); + assert(read_octet_string(seq![0x04u8, 0x81, 0x08, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF], 0, 11) is None) by (compute_only); + assert(read_octet_string( + seq![0x24u8, 0x0C, 0x04, 0x04, 0x01, 0x23, 0x45, 0x67, 0x04, 0x04, 0x89, 0xAB, 0xCD, 0xEF], + 0, + 14, + ) is None) by (compute_only); + assert(encode_octets(IA5_STRING, seq![0x74u8, 0x65, 0x73, 0x74, 0x31, 0x40, 0x72, 0x73, 0x61, 0x2E, 0x63, 0x6F, 0x6D]) =~= seq![ + 0x16u8, 0x0D, 0x74, 0x65, 0x73, 0x74, 0x31, 0x40, 0x72, 0x73, 0x61, 0x2E, 0x63, 0x6F, 0x6D, + ]) by (compute_only); + assert(encode_octets(UTC_TIME, seq![0x39u8, 0x31, 0x30, 0x35, 0x30, 0x36, 0x32, 0x33, 0x34, 0x35, 0x34, 0x30, 0x5A]) =~= seq![ + 0x17u8, 0x0D, 0x39, 0x31, 0x30, 0x35, 0x30, 0x36, 0x32, 0x33, 0x34, 0x35, 0x34, 0x30, 0x5A, + ]) by (compute_only); + assert(encode_sequence( + encode_set(encode_sequence(encode_oid(seq![2nat, 5, 4, 6]) + encode_octets(PRINTABLE_STRING, seq![0x55u8, 0x53]))) + + encode_set( + encode_sequence( + encode_oid(seq![2nat, 5, 4, 10]) + encode_octets( + PRINTABLE_STRING, + seq![ + 0x45u8, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x20, 0x4F, 0x72, 0x67, 0x61, 0x6E, 0x69, 0x7A, 0x61, 0x74, 0x69, + 0x6F, 0x6E, + ], + ), + ), + ) + encode_set( + encode_sequence( + encode_oid(seq![2nat, 5, 4, 3]) + encode_octets( + PRINTABLE_STRING, + seq![0x54u8, 0x65, 0x73, 0x74, 0x20, 0x55, 0x73, 0x65, 0x72, 0x20, 0x31], + ), + ), + ), + ) =~= seq![ + 0x30u8, 0x42, 0x31, 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x1D, 0x30, 0x1B, 0x06, + 0x03, 0x55, 0x04, 0x0A, 0x13, 0x14, 0x45, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x20, 0x4F, 0x72, 0x67, 0x61, 0x6E, 0x69, + 0x7A, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x0B, 0x54, 0x65, 0x73, + 0x74, 0x20, 0x55, 0x73, 0x65, 0x72, 0x20, 0x31, + ]) by (compute_only); + assert(encode_digest_info( + seq![2nat, 16, 840, 1, 101, 3, 4, 2, 1], + seq![ + 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + ], + ) =~= seq![ + 0x30u8, 0x31, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20, 0, 1, 2, + 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + ]) by (compute_only); +} + +} // verus! diff --git a/crates/clazel-der/src/independent.rs b/crates/clazel-der/src/independent.rs new file mode 100644 index 0000000..2265a04 --- /dev/null +++ b/crates/clazel-der/src/independent.rs @@ -0,0 +1,1446 @@ +//! A second transcription of DER, from a different text: Burton S. Kaliski +//! Jr., "A Layman's Guide to a Subset of ASN.1, BER, and DER" (RSA +//! Laboratories Technical Note, revised 1 November 1993; the copy read is +//! ), sections 2 to 5. +//! Its definitions (everything before the equivalence section at the end) +//! were written from the Guide alone, before `spec.rs` existed; they share no +//! definition with it. The one later change made them closer to the Guide, +//! not to the spec: the limits (tag number, arc) are checks on the finished +//! number, not early exits. The equivalence lemmas at the end necessarily +//! name the spec's functions, and only they do. +//! +//! ## What the Guide says and this file models +//! +//! - §3.1, "Identifier octets": a low-tag-number form (one octet: bits 8-7 +//! the class by Table 2, bit 6 primitive or constructed, bits 5-1 the tag +//! number for numbers 0 to 30) and a high-tag-number form (the first octet +//! as before with bits 5-1 all ones, then the tag number "base 128, most +//! significant digit first, with as few digits as possible, and with the +//! bit 8 of each octet except the last set to 1", for numbers 31 and +//! greater). [`k_ident`]. +//! - §3.1 and §4, "Length octets": the short form for 0 to 127, and for 128 +//! or more the long form "of length ... encoded in the minimum number of +//! octets": a first octet with bit 8 set and bits 7-1 the number of +//! additional octets, then the length base 256, most significant digit +//! first. §3.3 gives the indefinite form (the single octet 0x80), which §4 +//! does not allow ("exactly one way to represent any ASN.1 value", and the +//! definite-length methods only for the types it lists). The long form is +//! "Two to 127 octets" in all, so at most 126 additional octets: the first +//! octet 0xFF is not a length. [`k_length`]. +//! - §5.7 INTEGER: "the value of the integer, base 256, in two's complement +//! form, most significant digit first, with the minimum number of octets". +//! Here that is not a rule about the first two octets but a fact about the +//! value: the contents are accepted exactly when they equal the one +//! minimal two's complement spelling of the value they denote +//! ([`k_int_bytes`], [`k_integer_ok`]). +//! - §5.4 BIT STRING (DER): the first contents octet is the number of unused +//! bits (0 to 7, the amount by which the length is below the next multiple +//! of eight), the following octets hold the bits "padded with zero-valued +//! bits". An empty bit string is the single octet 0 ([`k_bitstring_ok`]). +//! - §5.8 NULL: empty contents. §5.10 OCTET STRING, §5.6 IA5String, §5.11 +//! PrintableString, §5.17 UTCTime: DER takes the primitive form, the +//! contents are the characters ([`k_primitive_string`]). +//! - §5.9 OBJECT IDENTIFIER: "The first octet has value 40 * value1 + +//! value2 ... The following octets, if any, encode value3, ..., valuen. +//! Each value is encoded base 128, most significant digit first, with as +//! few digits as possible, and the most significant bit of each octet +//! except the last in the value's encoding set to 1." [`k_oid`]. +//! - §5.12 SEQUENCE and §5.14 SET: constructed, definite length (§4); +//! §5.1 and §5.2: an implicitly tagged type changes the tag and keeps the +//! form of the underlying type, an explicitly tagged type is constructed +//! and holds the underlying encoding. +//! +//! ## What the Guide leaves out, and the limits shared with the spec +//! +//! - The Guide has no BOOLEAN, UTF8String or GeneralizedTime (X.690 §8.2, +//! §11.1 and X.680 are the only sources for them here), so they have no +//! second transcription and the equivalence lemmas do not mention them. +//! - Its first-octet rule for an OBJECT IDENTIFIER ("40 * value1 + value2", +//! one octet) holds for values below 128; the equivalence lemma for OIDs +//! is stated for contents whose first octet is below 128, which includes +//! every arc pair under the roots 0 and 1 and 2.0 to 2.47. +//! - The Guide has no upper bound on a tag number, a length or an arc. This +//! file stops at the same limits as the spec, which are choices of this +//! project and not the Guide's: tag numbers below 2^14, at most 8 length +//! octets, arcs below 2^64. + +#[allow(unused_imports)] +use super::spec::*; +#[allow(unused_imports)] +use vstd::prelude::*; + +verus! { + +// --------------------------------------------------------------------------- +// Numbers "base 128" and "base 256, most significant digit first". +// --------------------------------------------------------------------------- + +/// The number spelled by the octets `b[from..to)` in the given radix, most +/// significant digit first, folded from the left starting from `acc`. For +/// radix 128 each octet contributes its low seven bits. +pub open spec fn k_digits(b: Seq, from: nat, to: nat, radix: nat, acc: nat) -> nat + decreases to - from, +{ + if from >= to { + acc + } else { + let o = b[from as int] as nat; + let digit = if radix == 128 { o % 128 } else { o }; + k_digits(b, from + 1, to, radix, acc * radix + digit) + } +} + +// --------------------------------------------------------------------------- +// §3.1: identifier octets. +// --------------------------------------------------------------------------- + +/// What an element's identifier says: Table 2's class bits (0 universal, 1 +/// application, 2 context-specific, 3 private), whether the encoding is +/// constructed, and the tag number. +pub struct KIdent { + /// Bits 8 and 7 of the first identifier octet (Table 2). + pub class_bits: u8, + /// Whether the encoding is constructed (bit 6). + pub constructed: bool, + /// The tag number. + pub number: nat, +} + +/// The high tag number's digits starting at octet `i`: "base 128, most +/// significant digit first, with as few digits as possible, and with the bit +/// 8 of each octet except the last set to 1". `first` is whether octet `i` +/// is the leading digit, which may not be zero (the "as few digits as +/// possible" rule). Returns the number and the index after the last octet. +pub open spec fn k_high_tag(b: Seq, i: nat, end: nat, acc: nat, first: bool) -> Option<(nat, nat)> + decreases end - i, +{ + if i >= end { + None + } else { + let o = b[i as int]; + let digit = (o % 128) as nat; + if first && digit == 0 { + None + } else { + let acc2 = acc * 128 + digit; + if o >= 128 { + k_high_tag(b, i + 1, end, acc2, false) + } else { + Some((acc2, i + 1)) + } + } + } +} + +/// The identifier at octet `i`, and the index after it. +pub open spec fn k_ident(b: Seq, i: nat, end: nat) -> Option<(KIdent, nat)> { + if i >= end { + None + } else { + let o = b[i as int]; + let class_bits = o / 64; + let constructed = (o / 32) % 2 == 1; + let low = (o % 32) as nat; + if low < 31 { + // Low-tag-number form. + Some((KIdent { class_bits, constructed, number: low }, i + 1)) + } else { + // High-tag-number form: tag numbers 31 and greater. + match k_high_tag(b, i + 1, end, 0, true) { + Some((number, next)) => { + // 31 and greater; and below 2^14, a limit of this + // project. + if number >= 31 && number < 16384 { + Some((KIdent { class_bits, constructed, number }, next)) + } else { + None + } + }, + None => None, + } + } + } +} + +// --------------------------------------------------------------------------- +// §3.1 and §4: length octets. +// --------------------------------------------------------------------------- + +/// The number of base-256 digits "with the minimum number of octets" that +/// spell `v`. +pub open spec fn k_octets_for(v: nat) -> nat + decreases v, +{ + if v < 256 { + 1 + } else { + 1 + k_octets_for(v / 256) + } +} + +/// The length at octet `i`: its value and the index after it. DER: short +/// form for 0 to 127; for 128 and above the long form with the minimum +/// number of octets; never the indefinite form. +pub open spec fn k_length(b: Seq, i: nat, end: nat) -> Option<(nat, nat)> { + if i >= end { + None + } else { + let first = b[i as int]; + if first < 128 { + // Short form: bit 8 is zero and bits 7-1 give the length. + Some((first as nat, i + 1)) + } else { + // Long form: bits 7-1 give the number of additional octets. + let k = (first - 128) as nat; + if k == 0 { + // 0x80 is the indefinite form (§3.3): not DER. + None + } else if k > 8 { + // At most 126 octets in the Guide (so never 0xFF); this + // project stops at 8. + None + } else if i + 1 + k > end { + None + } else { + let v = k_digits(b, i + 1, i + 1 + k, 256, 0); + // §4: 128 or greater, and "the minimum number of octets". + if v >= 128 && k == k_octets_for(v) { + Some((v, i + 1 + k)) + } else { + None + } + } + } + } +} + +// --------------------------------------------------------------------------- +// §3.1: an element, identifier then length then contents. +// --------------------------------------------------------------------------- + +/// One element at `i`: its identifier, the contents' first index and the +/// contents' end index (which is also where the next element starts, since +/// DER has no end-of-contents octets). +pub struct KElem { + /// The identifier. + pub ident: KIdent, + /// Index of the first contents octet. + pub content_start: nat, + /// Index after the last contents octet. + pub content_end: nat, +} + +/// The element at `i`, definite length, contents inside `end`. +pub open spec fn k_element(b: Seq, i: nat, end: nat) -> Option { + match k_ident(b, i, end) { + None => None, + Some((ident, after_ident)) => match k_length(b, after_ident, end) { + None => None, + Some((len, content_start)) => { + if content_start + len > end { + None + } else { + Some(KElem { ident, content_start, content_end: content_start + len }) + } + }, + }, + } +} + +// --------------------------------------------------------------------------- +// §5.7 INTEGER. +// --------------------------------------------------------------------------- + +/// The value of contents octets read as base 256 two's complement, most +/// significant digit first: the first octet counts as negative when its bit +/// 8 is set, and each further octet shifts the running value by one digit. +pub open spec fn k_twos(c: Seq, j: nat, acc: int) -> int + decreases c.len() - j, +{ + if j >= c.len() { + acc + } else { + k_twos(c, j + 1, acc * 256 + c[j as int] as int) + } +} + +/// The integer the contents `c` denote (0 for no octets). +pub open spec fn k_integer_value(c: Seq) -> int { + if c.len() == 0 { + 0 + } else { + let first = c[0] as int; + k_twos(c, 1, if first >= 128 { first - 256 } else { first }) + } +} + +/// A measure for [`k_int_bytes`]: the distance of `v` from zero. +pub open spec fn k_abs(v: int) -> nat { + if v >= 0 { v as nat } else { (-v) as nat } +} + +/// "With the minimum number of octets": the shortest base 256 two's +/// complement spelling of `v`. One octet holds -128 to 127; otherwise the +/// spelling of `v` is the spelling of `v` divided by 256 (rounding down) +/// followed by the last digit. +pub open spec fn k_int_bytes(v: int) -> Seq + decreases k_abs(v), +{ + if -128 <= v && v < 128 { + seq![(v % 256) as u8] + } else { + k_int_bytes(v / 256) + seq![(v % 256) as u8] + } +} + +/// Contents are a DER INTEGER when they are the minimal spelling of the +/// value they denote ("The value 0 is encoded as a single 00 octet"). +pub open spec fn k_integer_ok(c: Seq) -> bool { + c.len() >= 1 && c =~= k_int_bytes(k_integer_value(c)) +} + +// --------------------------------------------------------------------------- +// §5.4 BIT STRING. +// --------------------------------------------------------------------------- + +/// 2 to the power of a number of unused bits (0 to 7). +pub open spec fn k_pow2_small(u: u8) -> nat { + if u == 0 { + 1 + } else if u == 1 { + 2 + } else if u == 2 { + 4 + } else if u == 3 { + 8 + } else if u == 4 { + 16 + } else if u == 5 { + 32 + } else if u == 6 { + 64 + } else { + 128 + } +} + +/// Contents of a primitive DER BIT STRING: a first octet giving the unused +/// bits (0 to 7); with no further octets the string is empty and nothing is +/// unused; otherwise the unused bits are the low bits of the last octet, +/// "padded with zero-valued bits". +pub open spec fn k_bitstring_ok(c: Seq) -> bool { + c.len() >= 1 && c[0] <= 7 && (c.len() == 1 ==> c[0] == 0) && (c.len() >= 2 ==> (c.last() as nat) % k_pow2_small( + c[0], + ) == 0) +} + +// --------------------------------------------------------------------------- +// §5.8, §5.10, §5.6, §5.11, §5.17: NULL and the string types. +// --------------------------------------------------------------------------- + +/// NULL: "Contents octets are empty". +pub open spec fn k_null_ok(c: Seq) -> bool { + c.len() == 0 +} + +/// OCTET STRING, IA5String, PrintableString, UTCTime: DER requires the +/// primitive form and the contents are the characters, whatever they are. +pub open spec fn k_primitive_string(c: Seq) -> bool { + true +} + +// --------------------------------------------------------------------------- +// §5.9 OBJECT IDENTIFIER. +// --------------------------------------------------------------------------- + +/// The value encoded base 128 at octet `i` of `c`: "most significant digit +/// first, with as few digits as possible, and the most significant bit of +/// each octet except the last set to 1". Returns the value and the index +/// after the last octet. +pub open spec fn k_base128_value(c: Seq, i: nat, acc: nat, first: bool) -> Option<(nat, nat)> + decreases c.len() - i, +{ + if i >= c.len() { + None + } else { + let o = c[i as int]; + // "As few digits as possible": a leading digit of zero is only the + // whole value 0, never a padding digit before more digits. + if first && o == 128 { + None + } else { + let acc2 = acc * 128 + (o % 128) as nat; + if o >= 128 { + k_base128_value(c, i + 1, acc2, false) + } else { + Some((acc2, i + 1)) + } + } + } +} + +/// The values value3, ..., valuen from octet `i` to the end of `c`, after +/// `found`. A value of 2^64 or more is refused (a limit of this project). +pub open spec fn k_rest_values(c: Seq, i: nat, found: Seq) -> Option> + decreases c.len() - i, + via k_rest_values_decreases +{ + if i >= c.len() { + Some(found) + } else { + match k_base128_value(c, i, 0, true) { + None => None, + Some((v, next)) => { + if v >= 0x1_0000_0000_0000_0000 { + None + } else { + k_rest_values(c, next, found.push(v)) + } + }, + } + } +} + +/// A value's digits end after they start and inside `c`. +pub proof fn k_base128_progress(c: Seq, i: nat, acc: nat, first: bool) + ensures + match k_base128_value(c, i, acc, first) { + Some((_, next)) => i < next && next <= c.len(), + None => true, + }, + decreases c.len() - i, +{ + if i < c.len() { + let o = c[i as int]; + if !(first && o == 128) { + let acc2 = acc * 128 + (o % 128) as nat; + if o >= 128 { + k_base128_progress(c, i + 1, acc2, false); + } + } + } +} + +/// Termination of [`k_rest_values`]. +#[via_fn] +pub proof fn k_rest_values_decreases(c: Seq, i: nat, found: Seq) { + if i < c.len() { + k_base128_progress(c, i, 0, true); + } +} + +/// The components of an object identifier whose encoding has a single first +/// octet, as the Guide states it: that octet is `40 * value1 + value2` with +/// `value1` limited to 0, 1 and 2 and `value2` below 40 for 0 and 1; the +/// remaining octets encode value3 to valuen. `None` when the first octet is +/// 128 or more (the Guide's rule does not cover it). +pub open spec fn k_oid(c: Seq) -> Option> { + if c.len() == 0 || c[0] >= 128 { + None + } else { + let f = c[0] as nat; + let value1: nat = if f / 40 >= 2 { 2 } else { f / 40 }; + let value2: nat = (f - 40 * value1) as nat; + k_rest_values(c, 1, seq![value1, value2]) + } +} + +// --------------------------------------------------------------------------- +// §5: the types, as an element with the right identifier and contents. +// --------------------------------------------------------------------------- + +/// The element at `i`, required to be universal class (bits 00), of the +/// given form and number. +pub open spec fn k_expect_universal(b: Seq, i: nat, end: nat, constructed: bool, number: nat) -> Option { + match k_element(b, i, end) { + None => None, + Some(e) => { + if e.ident.class_bits == 0 && e.ident.constructed == constructed && e.ident.number == number { + Some(e) + } else { + None + } + }, + } +} + +/// The element at `i`, required to be context-specific (bits 10) with the +/// given number. §5.1: for an implicitly tagged type the form is the +/// underlying type's, so it is returned for the caller to inspect; §5.2: an +/// explicitly tagged type is constructed. +pub open spec fn k_expect_context(b: Seq, i: nat, end: nat, number: nat) -> Option { + match k_element(b, i, end) { + None => None, + Some(e) => { + if e.ident.class_bits == 2 && e.ident.number == number { + Some(e) + } else { + None + } + }, + } +} + +/// An INTEGER (§5.7): a primitive universal element of tag number 2 whose +/// contents satisfy [`k_integer_ok`]. +pub open spec fn k_read_integer(b: Seq, i: nat, end: nat) -> Option { + match k_expect_universal(b, i, end, false, 2) { + None => None, + Some(e) => { + if k_integer_ok(b.subrange(e.content_start as int, e.content_end as int)) { + Some(e) + } else { + None + } + }, + } +} + +/// A BIT STRING (§5.4): tag number 3, contents satisfying [`k_bitstring_ok`]. +pub open spec fn k_read_bit_string(b: Seq, i: nat, end: nat) -> Option { + match k_expect_universal(b, i, end, false, 3) { + None => None, + Some(e) => { + if k_bitstring_ok(b.subrange(e.content_start as int, e.content_end as int)) { + Some(e) + } else { + None + } + }, + } +} + +/// NULL (§5.8): tag number 5, no contents. +pub open spec fn k_read_null(b: Seq, i: nat, end: nat) -> Option { + match k_expect_universal(b, i, end, false, 5) { + None => None, + Some(e) => { + if k_null_ok(b.subrange(e.content_start as int, e.content_end as int)) { + Some(e) + } else { + None + } + }, + } +} + +/// A string type in the primitive form (§5.10 OCTET STRING is number 4, +/// §5.11 PrintableString 19, §5.6 IA5String 22, §5.17 UTCTime 23). +pub open spec fn k_read_string(b: Seq, i: nat, end: nat, number: nat) -> Option { + match k_expect_universal(b, i, end, false, number) { + None => None, + Some(e) => { + if k_primitive_string(b.subrange(e.content_start as int, e.content_end as int)) { + Some(e) + } else { + None + } + }, + } +} + + +// =========================================================================== +// The two transcriptions agree. +// +// What is proven, and on what class of inputs. The Guide covers INTEGER, +// BIT STRING, NULL, OCTET STRING, IA5String, PrintableString, UTCTime, +// OBJECT IDENTIFIER, SEQUENCE, SET and tagged types (not BOOLEAN, UTF8String +// or GeneralizedTime), so these are the types compared. +// +// - Identifier octets, length octets and the framing of an element: on every +// input (`lemma_ident_equiv`, `lemma_length_equiv`, `lemma_element_equiv`). +// - INTEGER, BIT STRING and NULL contents: on every contents +// (`lemma_integer_equiv`, `lemma_bitstring_equiv`, `lemma_null_equiv`). +// - OBJECT IDENTIFIER contents: on those whose first octet is below 128, +// where the Guide's "first octet is 40 * value1 + value2" applies +// (`lemma_oid_equiv`). For a first octet of 128 or more the Guide's rule +// does not say how the first two components are encoded, so there is +// nothing to compare. +// - The typed readers, built from the above, for the types listed: on every +// input (`lemma_read_integer_equiv` and the others below). +// =========================================================================== + +/// 128 to the power `n`. +pub open spec fn k_pow128(n: nat) -> nat + decreases n, +{ + if n == 0 { + 1 + } else { + 128 * k_pow128((n - 1) as nat) + } +} + +/// A base 128 number splits into its first digit, weighed by the number of +/// digits after it, and the number the rest spells. +pub proof fn lemma_k_cons128(b: Seq, from: nat, to: nat) + requires + from < to <= b.len(), + ensures + base128(b.subrange(from as int, to as int)) == (b[from as int] % 128) as nat * k_pow128((to - from - 1) as nat) + + base128(b.subrange(from as int + 1, to as int)), + decreases to - from, +{ + let s = b.subrange(from as int, to as int); + if to - from == 1 { + assert(b.subrange(from as int + 1, to as int) =~= Seq::::empty()); + assert(s =~= seq![b[from as int]]); + super::lemmas_tlv::lemma_base128_single(b[from as int]); + assert((to - from - 1) as nat == 0nat); + assert(k_pow128(0) == 1); + assert(base128(Seq::::empty()) == 0); + } else { + let t = (to - 1) as nat; + assert(s.drop_last() =~= b.subrange(from as int, t as int)); + lemma_k_cons128(b, from, t); + let tail = b.subrange(from as int + 1, to as int); + assert(tail.drop_last() =~= b.subrange(from as int + 1, t as int)); + assert(s.last() == b[t as int]); + assert(tail.last() == b[t as int]); + let d = (b[from as int] % 128) as nat; + let p = k_pow128((to - from - 2) as nat); + let r = base128(b.subrange(from as int + 1, t as int)); + let l = (b[t as int] % 128) as nat; + assert((t - from - 1) as nat == (to - from - 2) as nat); + assert(base128(s) == base128(b.subrange(from as int, t as int)) * 128 + l); + assert(base128(tail) == r * 128 + l); + assert(base128(b.subrange(from as int, t as int)) == d * p + r); + assert(k_pow128((to - from - 1) as nat) == 128 * p); + assert((d * p + r) * 128 + l == d * (128 * p) + (r * 128 + l)) by (nonlinear_arith); + } +} + +/// The same for base 256. +pub proof fn lemma_k_cons256(b: Seq, from: nat, to: nat) + requires + from < to <= b.len(), + ensures + be_value(b.subrange(from as int, to as int)) == b[from as int] as nat * pow256((to - from - 1) as nat) + be_value( + b.subrange(from as int + 1, to as int), + ), + decreases to - from, +{ + let s = b.subrange(from as int, to as int); + if to - from == 1 { + assert(b.subrange(from as int + 1, to as int) =~= Seq::::empty()); + assert(s =~= seq![b[from as int]]); + super::lemmas_typed::lemma_be_value_single(b[from as int]); + assert((to - from - 1) as nat == 0nat); + assert(pow256(0) == 1); + assert(be_value(Seq::::empty()) == 0); + } else { + let t = (to - 1) as nat; + assert(s.drop_last() =~= b.subrange(from as int, t as int)); + lemma_k_cons256(b, from, t); + let tail = b.subrange(from as int + 1, to as int); + assert(tail.drop_last() =~= b.subrange(from as int + 1, t as int)); + assert(s.last() == b[t as int]); + assert(tail.last() == b[t as int]); + let d = b[from as int] as nat; + let p = pow256((to - from - 2) as nat); + let r = be_value(b.subrange(from as int + 1, t as int)); + let l = b[t as int] as nat; + assert((t - from - 1) as nat == (to - from - 2) as nat); + assert(be_value(s) == be_value(b.subrange(from as int, t as int)) * 256 + l); + assert(be_value(tail) == r * 256 + l); + assert(be_value(b.subrange(from as int, t as int)) == d * p + r); + assert(pow256((to - from - 1) as nat) == 256 * p); + assert((d * p + r) * 256 + l == d * (256 * p) + (r * 256 + l)) by (nonlinear_arith); + } +} + +/// Folding base 256 digits from the left, from `acc`, is `acc` shifted by +/// the digits plus the number they spell. +pub proof fn lemma_k_digits256(b: Seq, from: nat, to: nat, acc: nat) + requires + from <= to <= b.len(), + ensures + k_digits(b, from, to, 256, acc) == acc * pow256((to - from) as nat) + be_value(b.subrange(from as int, to as int)), + decreases to - from, +{ + if from == to { + assert(b.subrange(from as int, to as int) =~= Seq::::empty()); + } else { + let o = b[from as int] as nat; + lemma_k_digits256(b, from + 1, to, acc * 256 + o); + lemma_k_cons256(b, from, to); + let p = pow256((to - from - 1) as nat); + let r = be_value(b.subrange(from as int + 1, to as int)); + assert(pow256((to - from) as nat) == 256 * p); + assert((acc * 256 + o) * p + r == acc * (256 * p) + (o * p + r)) by (nonlinear_arith); + } +} + +/// A high tag number's digits: the series ends where the standard's does, +/// and the number is the digits' base 128 value (shifted by what came +/// before). +pub proof fn lemma_k_high_tag_run(b: Seq, i: nat, end: nat, acc: nat, first: bool) + requires + end <= b.len(), + ensures + k_high_tag(b, i, end, acc, first) == match series_end(b, i, end) { + None => None, + Some(j) => { + if first && b[i as int] % 128 == 0 { + None + } else { + Some((acc * k_pow128((j - i + 1) as nat) + base128(b.subrange(i as int, j as int + 1)), j + 1)) + } + }, + }, + decreases end - i, +{ + if i < end { + let o = b[i as int]; + if o < 128 { + assert(b.subrange(i as int, i as int + 1) =~= seq![o]); + super::lemmas_tlv::lemma_base128_single(o); + assert(series_end(b, i, end) == Some(i)); + assert(k_pow128(0) == 1); + assert(k_pow128(1) == 128); + assert((i - i + 1) as nat == 1nat); + } else { + let acc2 = acc * 128 + (o % 128) as nat; + lemma_k_high_tag_run(b, i + 1, end, acc2, false); + super::lemmas_oid::lemma_series_end_props(b, i + 1, end); + if let Some(j) = series_end(b, i + 1, end) { + lemma_k_cons128(b, i, j + 1); + let p = k_pow128((j - i) as nat); + let d = (o % 128) as nat; + assert(k_pow128((j - i + 1) as nat) == 128 * p); + assert((acc * 128 + d) * p == acc * (128 * p) + d * p) by (nonlinear_arith); + } + } + } +} + +/// An OBJECT IDENTIFIER's value: the same for the series at `i` of `c`. +pub proof fn lemma_k_base128_run(c: Seq, i: nat, acc: nat, first: bool) + ensures + k_base128_value(c, i, acc, first) == match series_end(c, i, c.len()) { + None => None, + Some(j) => { + if first && c[i as int] == 128 { + None + } else { + Some((acc * k_pow128((j - i + 1) as nat) + base128(c.subrange(i as int, j as int + 1)), j + 1)) + } + }, + }, + decreases c.len() - i, +{ + if i < c.len() { + let o = c[i as int]; + if o < 128 { + assert(c.subrange(i as int, i as int + 1) =~= seq![o]); + super::lemmas_tlv::lemma_base128_single(o); + assert(series_end(c, i, c.len()) == Some(i)); + assert(k_pow128(0) == 1); + assert(k_pow128(1) == 128); + assert((i - i + 1) as nat == 1nat); + } else { + let acc2 = acc * 128 + (o % 128) as nat; + lemma_k_base128_run(c, i + 1, acc2, false); + super::lemmas_oid::lemma_series_end_props(c, i + 1, c.len()); + if let Some(j) = series_end(c, i + 1, c.len()) { + lemma_k_cons128(c, i, j + 1); + let p = k_pow128((j - i) as nat); + let d = (o % 128) as nat; + assert(k_pow128((j - i + 1) as nat) == 128 * p); + assert((acc * 128 + d) * p == acc * (128 * p) + d * p) by (nonlinear_arith); + } + } + } +} + +// --------------------------------------------------------------------------- +// Identifier octets. +// --------------------------------------------------------------------------- + +/// The Guide's identifier octets and the standard's agree on every input: +/// the same class bits, form and number, ending at the same index. +pub proof fn lemma_ident_equiv(b: Seq, i: nat, end: nat) + requires + end <= b.len(), + ensures + match read_identifier(b, i, end) { + Some((tag, n)) => k_ident(b, i, end) == Some( + ( + KIdent { class_bits: class_bits(tag.class), constructed: tag.constructed, number: tag.number as nat }, + i + n, + ), + ), + None => k_ident(b, i, end) is None, + }, +{ + if i < end { + let o = b[i as int]; + assert(o / 64 <= 3); + super::lemmas_tlv::lemma_class_from_bits_roundtrip(o / 64); + if o % 32 == 31 { + lemma_k_high_tag_run(b, i + 1, end, 0, true); + super::lemmas::lemma_series_end_in_range(b, i + 1, end); + if let Some(j) = series_end(b, i + 1, end) { + assert(0 * k_pow128((j - (i + 1) + 1) as nat) == 0); + let number = base128(b.subrange(i as int + 1, j as int + 1)); + assert(b[i as int + 1] == b[(i + 1) as int]); + assert(k_high_tag(b, i + 1, end, 0, true) == if b[(i + 1) as int] % 128 == 0 { + None + } else { + Some((number, j + 1)) + }); + } + } else { + assert(class_bits(class_from_bits(o / 64)) == o / 64); + } + } +} + +// --------------------------------------------------------------------------- +// Length octets. +// --------------------------------------------------------------------------- + +/// A number below 256^m takes at most m octets, and one of at least +/// 256^(m-1) takes exactly m. +pub proof fn lemma_octets_for_bounds(v: nat, m: nat) + requires + m >= 1, + ensures + v < pow256(m) ==> k_octets_for(v) <= m, + (v >= pow256((m - 1) as nat) && v < pow256(m)) ==> k_octets_for(v) == m, + decreases m, +{ + super::lemmas::lemma_pow256_8(); + assert(pow256(1) == 256); + assert(pow256(0) == 1); + if m >= 2 { + let p = pow256((m - 1) as nat); + assert(pow256(m) == 256 * p); + lemma_octets_for_bounds(v / 256, (m - 1) as nat); + super::lemmas_tlv::lemma_pow256_monotone(1, (m - 1) as nat); + assert(pow256((m - 1) as nat) == 256 * pow256((m - 2) as nat)); + } +} + +/// The Guide's length octets and the standard's agree on every input. +pub proof fn lemma_length_equiv(b: Seq, i: nat, end: nat) + requires + end <= b.len(), + ensures + match read_length(b, i, end) { + Some((v, n)) => k_length(b, i, end) == Some((v, i + n)), + None => k_length(b, i, end) is None, + }, +{ + super::lemmas::lemma_pow256_8(); + if i < end { + let f = b[i as int]; + if f >= 129 && f <= 136 && i + 1 + ((f - 128) as nat) <= end { + let k = (f - 128) as nat; + let sub = b.subrange(i as int + 1, i as int + 1 + k as int); + lemma_k_digits256(b, i + 1, i + 1 + k, 0); + assert(0 * pow256(k) == 0); + let v = be_value(sub); + super::lemmas::lemma_be_value_bound(sub); + assert(sub[0] == b[i as int + 1]); + lemma_octets_for_bounds(v, k); + if sub[0] != 0 { + super::lemmas_tlv::lemma_be_value_leading_nonzero(sub); + } else { + super::lemmas_tlv::lemma_be_value_leading_zero(sub); + if k >= 2 { + lemma_octets_for_bounds(v, (k - 1) as nat); + } else { + assert(pow256(0) == 1); + assert(v == 0); + } + } + } + } +} + +// --------------------------------------------------------------------------- +// An element. +// --------------------------------------------------------------------------- + +/// The Guide's element and the standard's value agree on every input: same +/// identifier, same contents range. +pub proof fn lemma_element_equiv(b: Seq, i: nat, end: nat) + requires + end <= b.len(), + ensures + match read_tlv(b, i, end) { + Some(t) => k_element(b, i, end) == Some( + KElem { + ident: KIdent { class_bits: class_bits(t.tag.class), constructed: t.tag.constructed, number: t.tag.number as nat }, + content_start: t.cstart, + content_end: t.cend, + }, + ), + None => k_element(b, i, end) is None, + }, +{ + lemma_ident_equiv(b, i, end); + if let Some((tag, id_len)) = read_identifier(b, i, end) { + super::lemmas::lemma_read_identifier_bounds(b, i, end); + lemma_length_equiv(b, i + id_len, end); + } +} + +// --------------------------------------------------------------------------- +// INTEGER contents. +// --------------------------------------------------------------------------- + +/// The running value of the fold over the contents, when the last octet is +/// appended, shifts by one digit. +pub proof fn lemma_k_twos_push(d: Seq, x: u8, j: nat, a: int) + requires + j <= d.len(), + ensures + k_twos(d.push(x), j, a) == k_twos(d, j, a) * 256 + x as int, + decreases d.len() - j, +{ + if j < d.len() { + assert(d.push(x)[j as int] == d[j as int]); + lemma_k_twos_push(d, x, j + 1, a * 256 + d[j as int] as int); + } else { + assert(d.push(x)[j as int] == x); + assert(k_twos(d, j, a) == a); + assert(k_twos(d.push(x), j, a) == k_twos(d.push(x), j + 1, a * 256 + x as int)); + assert(k_twos(d.push(x), j + 1, a * 256 + x as int) == a * 256 + x as int); + } +} + +/// The integer value of contents with one more octet appended. +pub proof fn lemma_k_value_push(d: Seq, x: u8) + requires + d.len() >= 1, + ensures + k_integer_value(d.push(x)) == k_integer_value(d) * 256 + x as int, +{ + assert(d.push(x)[0] == d[0]); + lemma_k_twos_push(d, x, 1, if d[0] >= 128 { d[0] as int - 256 } else { d[0] as int }); +} + +/// Dropping the first octet shifts the fold's index. +pub proof fn lemma_k_twos_shift(c: Seq, j: nat, acc: int) + requires + c.len() >= 1, + ensures + k_twos(c.skip(1), j, acc) == k_twos(c, j + 1, acc), + decreases c.len() - j, +{ + if j + 1 < c.len() { + assert(c.skip(1)[j as int] == c[j as int + 1]); + lemma_k_twos_shift(c, j + 1, acc * 256 + c[j as int + 1] as int); + } +} + +/// One octet spells its signed value, and that value is spelled by that +/// one octet alone. +pub proof fn lemma_k_single(c: Seq) + requires + c.len() == 1, + ensures + k_integer_value(c) == (if c[0] >= 128 { c[0] as int - 256 } else { c[0] as int }), + -128 <= k_integer_value(c) < 128, + c =~= k_int_bytes(k_integer_value(c)), +{ + let v = k_integer_value(c); + assert(v == (if c[0] >= 128 { c[0] as int - 256 } else { c[0] as int })); + assert((v % 256) as u8 == c[0]); +} + +/// The minimal spelling of a value is never longer than any spelling of it. +pub proof fn lemma_k_int_bytes_len(t: Seq) + requires + t.len() >= 1, + ensures + k_int_bytes(k_integer_value(t)).len() <= t.len(), + decreases t.len(), +{ + if t.len() == 1 { + lemma_k_single(t); + } else { + let d = t.drop_last(); + let last = t.last(); + assert(t =~= d.push(last)); + lemma_k_value_push(d, last); + lemma_k_int_bytes_len(d); + let v = k_integer_value(t); + if !(-128 <= v && v < 128) { + let vd = k_integer_value(d); + assert(v / 256 == vd) by (nonlinear_arith) + requires + v == vd * 256 + last as int, + 0 <= last < 256, + ; + } + } +} + +/// A leading octet that only repeats the sign of the next one does not +/// change the value. +pub proof fn lemma_k_leading(c: Seq) + requires + c.len() >= 2, + (c[0] == 0 && c[1] < 128) || (c[0] == 255 && c[1] >= 128), + ensures + k_integer_value(c) == k_integer_value(c.skip(1)), +{ + let tail = c.skip(1); + assert(tail[0] == c[1]); + lemma_k_twos_shift(c, 1, if c[1] >= 128 { c[1] as int - 256 } else { c[1] as int }); + assert(k_twos(c, 1, if c[0] >= 128 { c[0] as int - 256 } else { c[0] as int }) == k_twos( + c, + 2, + (if c[0] >= 128 { c[0] as int - 256 } else { c[0] as int }) * 256 + c[1] as int, + )); +} + +/// The Guide's INTEGER rule (the contents are the minimal two's complement +/// spelling of their value) and the standard's (X.690 8.3.2: no redundant +/// first octet) accept exactly the same contents. +pub proof fn lemma_integer_equiv(c: Seq) + ensures + k_integer_ok(c) == integer_contents_ok(c), + decreases c.len(), +{ + if c.len() == 1 { + lemma_k_single(c); + } else if c.len() >= 2 { + let d = c.drop_last(); + let last = c.last(); + assert(c =~= d.push(last)); + lemma_k_value_push(d, last); + let v = k_integer_value(c); + let vd = k_integer_value(d); + let bad = (c[0] == 0 && c[1] < 128) || (c[0] == 255 && c[1] >= 128); + if bad { + // Not minimal: the tail alone spells the same value. + lemma_k_leading(c); + lemma_k_int_bytes_len(c.skip(1)); + assert(c.skip(1).len() == c.len() - 1); + } else { + lemma_integer_equiv(d); + assert(d.len() >= 1); + if d.len() >= 2 { + assert(d[0] == c[0]); + assert(d[1] == c[1]); + } + // d is minimal and so is c. + assert(integer_contents_ok(d)); + assert(k_integer_ok(d)); + assert(d =~= k_int_bytes(vd)); + if vd == 0 { + assert(d =~= seq![0u8]); + assert(c =~= seq![0u8, last]); + assert(last >= 128); + } + if vd == -1 { + assert(d =~= seq![255u8]); + assert(c =~= seq![255u8, last]); + assert(last < 128); + } + assert(!(-128 <= v && v < 128)); + assert(v / 256 == vd) by (nonlinear_arith) + requires + v == vd * 256 + last as int, + 0 <= last < 256, + ; + assert(v % 256 == last as int) by (nonlinear_arith) + requires + v == vd * 256 + last as int, + 0 <= last < 256, + ; + assert(c =~= k_int_bytes(vd) + seq![last]); + } + } +} + +// --------------------------------------------------------------------------- +// BIT STRING and NULL contents. +// --------------------------------------------------------------------------- + +/// The Guide's BIT STRING rule and the standard's accept the same contents. +pub proof fn lemma_bitstring_equiv(c: Seq) + ensures + k_bitstring_ok(c) == bit_string_contents_ok(c), +{ + if c.len() >= 1 { + assert(k_pow2_small(c[0]) == pow2_unused(c[0])); + } +} + +/// The Guide's NULL rule and the standard's accept the same contents: none. +pub proof fn lemma_null_equiv(c: Seq) + ensures + k_null_ok(c) == (c.len() == 0), +{ +} + +// --------------------------------------------------------------------------- +// OBJECT IDENTIFIER contents. +// --------------------------------------------------------------------------- + +/// The Guide's list of values value3 to valuen after `found`, read from +/// octet `i`, is `found` followed by the standard's subidentifiers of the +/// rest. +pub proof fn lemma_k_rest_values(c: Seq, i: nat, found: Seq) + requires + i <= c.len(), + ensures + k_rest_values(c, i, found) == match subidentifiers(c.skip(i as int)) { + None => None, + Some(rest) => Some(found + rest), + }, + decreases c.len() - i, +{ + if i >= c.len() { + assert(c.skip(i as int) =~= Seq::::empty()); + assert(found + Seq::::empty() =~= found); + } else { + super::lemmas::lemma_subidentifiers_step(c, i); + lemma_k_base128_run(c, i, 0, true); + super::lemmas_oid::lemma_series_end_props(c, i, c.len()); + if let Some(j) = series_end(c, i, c.len()) { + let v = base128(c.subrange(i as int, j as int + 1)); + assert(0 * k_pow128((j - i + 1) as nat) == 0); + if c[i as int] != 128 && v < 0x1_0000_0000_0000_0000 { + lemma_k_rest_values(c, j + 1, found.push(v)); + if let Some(rest) = subidentifiers(c.skip(j as int + 1)) { + assert(found.push(v) + rest =~= found + (seq![v] + rest)); + } + } + } + } +} + +/// The Guide's object identifier rule (first octet `40 * value1 + value2`, +/// the others base 128) and the standard's (§8.19.2 to §8.19.5) give the same +/// components, for contents whose first octet is below 128: the case the +/// Guide's rule states. +pub proof fn lemma_oid_equiv(c: Seq) + requires + c.len() >= 1, + c[0] < 128, + ensures + k_oid(c) == oid_components(c), +{ + let f = c[0] as nat; + super::lemmas::lemma_subidentifiers_step(c, 0); + assert(c.skip(0) =~= c); + assert(c.subrange(0, 1) =~= seq![c[0]]); + super::lemmas_tlv::lemma_base128_single(c[0]); + assert(series_end(c, 0, c.len()) == Some(0nat)); + lemma_k_rest_values(c, 1, seq![if f / 40 >= 2 { 2nat } else { f / 40 }, (f - 40 * (if f / 40 >= 2 { 2nat } else { f / 40 })) as nat]); + if let Some(rest) = subidentifiers(c.skip(1)) { + let subs = seq![f] + rest; + assert(subs.skip(1) =~= rest); + assert(subs[0] == f); + } +} + +// --------------------------------------------------------------------------- +// The typed readers. +// --------------------------------------------------------------------------- + +/// The standard's tag is universal and primitive of number `n` exactly when +/// the Guide's identifier says so. +proof fn lemma_tag_is_universal(tag: Tag, constructed: bool, n: u32) + ensures + (tag == Tag { class: Class::Universal, constructed, number: n }) == (class_bits(tag.class) == 0 && tag.constructed + == constructed && tag.number as nat == n as nat), +{ +} + +/// An INTEGER read by the Guide's rules is read by the standard's, and the +/// other way round, from every input, with the same extent. +pub proof fn lemma_read_integer_equiv(b: Seq, i: nat, end: nat) + requires + end <= b.len(), + ensures + match (read_integer(b, i, end), k_read_integer(b, i, end)) { + (Some((_, _, me, next)), Some(e)) => me == e.content_end && next == e.content_end, + (None, None) => true, + _ => false, + }, +{ + lemma_element_equiv(b, i, end); + if let Some(t) = read_tlv(b, i, end) { + lemma_tag_is_universal(t.tag, false, 2); + super::lemmas::lemma_read_tlv_bounds(b, i, end); + lemma_integer_equiv(b.subrange(t.cstart as int, t.cend as int)); + } +} + +/// A BIT STRING read by the Guide's rules is read by the standard's, and +/// the other way round, from every input, with the same extent. +pub proof fn lemma_read_bit_string_equiv(b: Seq, i: nat, end: nat) + requires + end <= b.len(), + ensures + match (read_bit_string(b, i, end), k_read_bit_string(b, i, end)) { + (Some((_, _, me, next)), Some(e)) => me == e.content_end && next == e.content_end, + (None, None) => true, + _ => false, + }, +{ + lemma_element_equiv(b, i, end); + if let Some(t) = read_tlv(b, i, end) { + lemma_tag_is_universal(t.tag, false, 3); + super::lemmas::lemma_read_tlv_bounds(b, i, end); + lemma_bitstring_equiv(b.subrange(t.cstart as int, t.cend as int)); + } +} + +/// A NULL read by the Guide's rules is read by the standard's, and the +/// other way round, from every input, with the same extent. +pub proof fn lemma_read_null_equiv(b: Seq, i: nat, end: nat) + requires + end <= b.len(), + ensures + match (read_null(b, i, end), k_read_null(b, i, end)) { + (Some(next), Some(e)) => next == e.content_end, + (None, None) => true, + _ => false, + }, +{ + lemma_element_equiv(b, i, end); + if let Some(t) = read_tlv(b, i, end) { + lemma_tag_is_universal(t.tag, false, 5); + super::lemmas::lemma_read_tlv_bounds(b, i, end); + lemma_null_equiv(b.subrange(t.cstart as int, t.cend as int)); + } +} + +/// An OCTET STRING, PrintableString, IA5String or UTCTime (numbers 4, 19, +/// 22 and 23) read by the Guide's rules is read by the standard's, and the +/// other way round, from every input, with the same contents range. +pub proof fn lemma_read_string_equiv(b: Seq, i: nat, end: nat, number: u32) + requires + end <= b.len(), + number == 4 || number == 19 || number == 22 || number == 23, + ensures + match (read_octets(b, i, end, number), k_read_string(b, i, end, number as nat)) { + (Some((cs, ce)), Some(e)) => cs == e.content_start && ce == e.content_end, + (None, None) => true, + _ => false, + }, +{ + lemma_element_equiv(b, i, end); + if let Some(t) = read_tlv(b, i, end) { + lemma_tag_is_universal(t.tag, false, number); + } +} + +/// A SEQUENCE (number 16) or SET (number 17) read by the Guide's rules is +/// read by the standard's, and the other way round, from every input, with +/// the same contents range. +pub proof fn lemma_expect_constructed_equiv(b: Seq, i: nat, end: nat, number: u32) + requires + end <= b.len(), + number == 16 || number == 17, + ensures + match (expect_constructed(b, i, end, number), k_expect_universal(b, i, end, true, number as nat)) { + (Some((cs, ce)), Some(e)) => cs == e.content_start && ce == e.content_end, + (None, None) => true, + _ => false, + }, +{ + lemma_element_equiv(b, i, end); + if let Some(t) = read_tlv(b, i, end) { + lemma_tag_is_universal(t.tag, true, number); + } +} + +/// A component with a context-specific tag `[number]`: the Guide finds it +/// exactly when the standard's optional-component reader does, with the +/// same form and contents range. (An absent component is "no element" to +/// the Guide's reader, so only the present case is compared.) +pub proof fn lemma_context_equiv(b: Seq, i: nat, end: nat, number: u32) + requires + end <= b.len(), + ensures + match k_expect_context(b, i, end, number as nat) { + Some(e) => read_optional_context(b, i, end, number) == Some(Some((e.ident.constructed, e.content_start, e.content_end))), + None => !(read_optional_context(b, i, end, number) matches Some(Some(_))), + }, +{ + lemma_element_equiv(b, i, end); + lemma_ident_equiv(b, i, end); + if i < end { + if let Some((tag, n)) = read_identifier(b, i, end) { + super::lemmas::lemma_read_identifier_bounds(b, i, end); + assert(class_bits(tag.class) == 2 <==> tag.class == Class::ContextSpecific); + } + } +} + + +/// An OBJECT IDENTIFIER (§5.9): a primitive universal element of tag number +/// 6 whose contents [`k_oid`] reads. +pub open spec fn k_read_oid(b: Seq, i: nat, end: nat) -> Option<(Seq, nat)> { + match k_expect_universal(b, i, end, false, 6) { + None => None, + Some(e) => match k_oid(b.subrange(e.content_start as int, e.content_end as int)) { + Some(arcs) => Some((arcs, e.content_end)), + None => None, + }, + } +} + +/// An OBJECT IDENTIFIER read by the Guide's rules is read by the standard's, +/// and the other way round, with the same components and extent, for every +/// input whose contents (if it is an element at all) are empty or begin with +/// an octet below 128: the case where the Guide's first-octet rule applies. +pub proof fn lemma_read_oid_equiv(b: Seq, i: nat, end: nat) + requires + end <= b.len(), + read_tlv(b, i, end) matches Some(t) ==> (t.cstart == t.cend || b[t.cstart as int] < 128), + ensures + match (read_oid(b, i, end), k_read_oid(b, i, end)) { + (Some((arcs, next)), Some((arcs2, next2))) => arcs == arcs2 && next == next2, + (None, None) => true, + _ => false, + }, +{ + lemma_element_equiv(b, i, end); + if let Some(t) = read_tlv(b, i, end) { + lemma_tag_is_universal(t.tag, false, 6); + super::lemmas::lemma_read_tlv_bounds(b, i, end); + let c = b.subrange(t.cstart as int, t.cend as int); + if t.tag == universal_primitive(OBJECT_IDENTIFIER) { + if t.cstart < t.cend { + assert(c[0] == b[t.cstart as int]); + lemma_oid_equiv(c); + } else { + assert(c =~= Seq::::empty()); + } + } + } +} + +/// The wrappers that fix a universal tag number agree with the Guide's +/// reader at that number: an OCTET STRING (4). +pub proof fn lemma_read_octet_string_equiv(b: Seq, i: nat, end: nat) + requires + end <= b.len(), + ensures + match (read_octet_string(b, i, end), k_read_string(b, i, end, 4)) { + (Some((cs, ce)), Some(e)) => cs == e.content_start && ce == e.content_end, + (None, None) => true, + _ => false, + }, +{ + lemma_read_string_equiv(b, i, end, 4); +} + +/// A PrintableString (19). +pub proof fn lemma_read_printable_string_equiv(b: Seq, i: nat, end: nat) + requires + end <= b.len(), + ensures + match (read_printable_string(b, i, end), k_read_string(b, i, end, 19)) { + (Some((cs, ce)), Some(e)) => cs == e.content_start && ce == e.content_end, + (None, None) => true, + _ => false, + }, +{ + lemma_read_string_equiv(b, i, end, 19); +} + +/// An IA5String (22). +pub proof fn lemma_read_ia5_string_equiv(b: Seq, i: nat, end: nat) + requires + end <= b.len(), + ensures + match (read_ia5_string(b, i, end), k_read_string(b, i, end, 22)) { + (Some((cs, ce)), Some(e)) => cs == e.content_start && ce == e.content_end, + (None, None) => true, + _ => false, + }, +{ + lemma_read_string_equiv(b, i, end, 22); +} + +/// A UTCTime (23). +pub proof fn lemma_read_utc_time_equiv(b: Seq, i: nat, end: nat) + requires + end <= b.len(), + ensures + match (read_utc_time(b, i, end), k_read_string(b, i, end, 23)) { + (Some((cs, ce)), Some(e)) => cs == e.content_start && ce == e.content_end, + (None, None) => true, + _ => false, + }, +{ + lemma_read_string_equiv(b, i, end, 23); +} + +/// A SEQUENCE (16). +pub proof fn lemma_expect_sequence_equiv(b: Seq, i: nat, end: nat) + requires + end <= b.len(), + ensures + match (expect_sequence(b, i, end), k_expect_universal(b, i, end, true, 16)) { + (Some((cs, ce)), Some(e)) => cs == e.content_start && ce == e.content_end, + (None, None) => true, + _ => false, + }, +{ + lemma_expect_constructed_equiv(b, i, end, 16); +} + +/// A SET (17). +pub proof fn lemma_expect_set_equiv(b: Seq, i: nat, end: nat) + requires + end <= b.len(), + ensures + match (expect_set(b, i, end), k_expect_universal(b, i, end, true, 17)) { + (Some((cs, ce)), Some(e)) => cs == e.content_start && ce == e.content_end, + (None, None) => true, + _ => false, + }, +{ + lemma_expect_constructed_equiv(b, i, end, 17); +} + + +/// The value the standard gives the contents (§8.3.3, "a two's complement +/// binary number": the octets' number, less 256^n if bit 8 of the first is +/// set) and the value the Guide gives them (§5.7, base 256 two's complement, +/// most significant digit first) are the same number, for every contents. +pub proof fn lemma_integer_value_equiv(c: Seq) + ensures + integer_value(c) == k_integer_value(c), + decreases c.len(), +{ + if c.len() == 1 { + lemma_k_single(c); + assert(c =~= seq![c[0]]); + super::lemmas_typed::lemma_be_value_single(c[0]); + assert(pow256(1) == 256 * pow256(0)); + assert(pow256(0) == 1); + } else if c.len() >= 2 { + let d = c.drop_last(); + let last = c.last(); + assert(c =~= d.push(last)); + lemma_integer_value_equiv(d); + lemma_k_value_push(d, last); + assert(d[0] == c[0]); + assert(be_value(c) == be_value(d) * 256 + last as nat); + assert(pow256(c.len()) == 256 * pow256(d.len())); + if c[0] >= 128 { + assert(integer_value(c) == (be_value(d) as int - pow256(d.len()) as int) * 256 + last as int) by (nonlinear_arith) + requires + integer_value(c) == be_value(c) as int - pow256(c.len()) as int, + be_value(c) == be_value(d) * 256 + last as nat, + pow256(c.len()) == 256 * pow256(d.len()), + ; + } + } +} + +} // verus! diff --git a/crates/clazel-der/src/lemmas.rs b/crates/clazel-der/src/lemmas.rs new file mode 100644 index 0000000..c580336 --- /dev/null +++ b/crates/clazel-der/src/lemmas.rs @@ -0,0 +1,389 @@ +//! Facts proven about the spec itself ([`super::spec`]). They are evidence +//! that the transcription is right, not part of what the implementation +//! must meet. The ones that matter are listed in `README.md`: no over-read +//! ([`lemma_read_tlv_no_overread`] and the typed readers'), the round trip +//! ([`lemma_read_tlv_encode_tlv`]), canonicity ([`lemma_encode_read_tlv`] +//! and the typed ones), the bounded reservation +//! ([`lemma_oid_components_bound`], [`lemma_encode_tlv_len`]) and the +//! shortest length ([`lemma_encode_length_shortest`], +//! [`lemma_read_length_encode_length`], [`lemma_encode_read_length`]). + +#[allow(unused_imports)] +use super::spec::*; +#[allow(unused_imports)] +use vstd::prelude::*; + +verus! { + +// --------------------------------------------------------------------------- +// Network byte order. +// --------------------------------------------------------------------------- + +/// Eight octets hold exactly the values of a `u64`. +pub proof fn lemma_pow256_8() + ensures + pow256(8) == 0x1_0000_0000_0000_0000, + pow256(7) == 0x100_0000_0000_0000, + pow256(6) == 0x1_0000_0000_0000, + pow256(5) == 0x100_0000_0000, + pow256(4) == 0x1_0000_0000, + pow256(3) == 0x100_0000, + pow256(2) == 0x1_0000, + pow256(1) == 256, +{ + assert(pow256(8) == 0x1_0000_0000_0000_0000) by (compute_only); + assert(pow256(7) == 0x100_0000_0000_0000) by (compute_only); + assert(pow256(6) == 0x1_0000_0000_0000) by (compute_only); + assert(pow256(5) == 0x100_0000_0000) by (compute_only); + assert(pow256(4) == 0x1_0000_0000) by (compute_only); + assert(pow256(3) == 0x100_0000) by (compute_only); + assert(pow256(2) == 0x1_0000) by (compute_only); + assert(pow256(1) == 256) by (compute_only); +} + +/// 256^n is at least 1. +pub proof fn lemma_pow256_positive(n: nat) + ensures + pow256(n) >= 1, + decreases n, +{ + if n > 0 { + lemma_pow256_positive((n - 1) as nat); + } +} + +/// Up to eight octets fit in a `u64`. +pub proof fn lemma_pow256_at_most_8(n: nat) + requires + n <= 8, + ensures + pow256(n) <= 0x1_0000_0000_0000_0000, + decreases 8 - n, +{ + if n < 8 { + lemma_pow256_at_most_8(n + 1); + lemma_pow256_positive(n); + assert(pow256(n + 1) == 256 * pow256(n)); + } else { + lemma_pow256_8(); + } +} + +/// `n` octets spell a number below 256^n. +pub proof fn lemma_be_value_bound(s: Seq) + ensures + be_value(s) < pow256(s.len()), + decreases s.len(), +{ + if s.len() > 0 { + lemma_be_value_bound(s.drop_last()); + assert(be_value(s) == be_value(s.drop_last()) * 256 + s.last() as nat); + assert(pow256(s.len()) == 256 * pow256((s.len() - 1) as nat)); + } +} + +/// `be_bytes(v, w)` has `w` octets. +pub proof fn lemma_be_bytes_len(v: nat, w: nat) + ensures + be_bytes(v, w).len() == w, + decreases w, +{ + if w > 0 { + lemma_be_bytes_len(v / 256, (w - 1) as nat); + } +} + +/// Reading back what [`be_bytes`] wrote: any `v` below 256^w survives. +pub proof fn lemma_be_value_of_be_bytes(v: nat, w: nat) + requires + v < pow256(w), + ensures + be_value(be_bytes(v, w)) == v, + decreases w, +{ + if w > 0 { + let rest = be_bytes(v / 256, (w - 1) as nat); + assert(pow256(w) == 256 * pow256((w - 1) as nat)); + assert(v / 256 < pow256((w - 1) as nat)); + lemma_be_value_of_be_bytes(v / 256, (w - 1) as nat); + lemma_be_bytes_len(v / 256, (w - 1) as nat); + let s = rest.push((v % 256) as u8); + assert(s.drop_last() =~= rest); + assert(s.last() == (v % 256) as u8); + assert(be_value(s) == be_value(rest) * 256 + (v % 256) as nat); + assert(v == (v / 256) * 256 + v % 256); + } +} + +/// Writing what [`be_value`] read: `s` is spelled back octet for octet. +pub proof fn lemma_be_bytes_of_be_value(s: Seq) + ensures + be_bytes(be_value(s), s.len()) =~= s, + decreases s.len(), +{ + if s.len() > 0 { + let d = s.drop_last(); + lemma_be_bytes_of_be_value(d); + let v = be_value(s); + assert(v == be_value(d) * 256 + s.last() as nat); + assert(v / 256 == be_value(d)) by (nonlinear_arith) + requires + v == be_value(d) * 256 + s.last() as nat, + s.last() < 256, + ; + assert(v % 256 == s.last() as nat) by (nonlinear_arith) + requires + v == be_value(d) * 256 + s.last() as nat, + s.last() < 256, + ; + assert(be_bytes(v, s.len()) =~= be_bytes(v / 256, d.len()).push((v % 256) as u8)); + assert(d.push(s.last()) =~= s); + } +} + +// --------------------------------------------------------------------------- +// Base 128 series. +// --------------------------------------------------------------------------- + +/// Appending an octet to a series: the number shifts by one digit. +pub proof fn lemma_base128_push(s: Seq, x: u8) + ensures + base128(s.push(x)) == base128(s) * 128 + (x % 128) as nat, +{ + assert(s.push(x).drop_last() =~= s); +} + +/// A longer series spells a number at least as large: the later digits only +/// append. (This is what lets a reader stop at once on a number that is too +/// large: nothing that follows makes it smaller.) +pub proof fn lemma_base128_monotone(b: Seq, start: int, i: int, j: int) + requires + 0 <= start <= i <= j <= b.len(), + ensures + base128(b.subrange(start, i)) <= base128(b.subrange(start, j)), + decreases j - i, +{ + if i < j { + lemma_base128_monotone(b, start, i, j - 1); + assert(b.subrange(start, j) =~= b.subrange(start, j - 1).push(b[j - 1])); + lemma_base128_push(b.subrange(start, j - 1), b[j - 1]); + } +} + +/// If every octet of `b[start..i)` has bit 8 set, the series that starts at +/// `start` ends where the one that starts at `i` does. +pub proof fn lemma_series_end_skip(b: Seq, start: nat, i: nat, end: nat) + requires + start <= i, + forall|k: int| start <= k < i ==> #[trigger] b[k] >= 128, + ensures + series_end(b, start, end) == series_end(b, i, end), + decreases i - start, +{ + if start < i { + if start < end { + assert(b[start as int] >= 128); + } + lemma_series_end_skip(b, start + 1, i, end); + } +} + +/// A series that ends ends at an index in `[i, end)` (re-export of the fact +/// the spec's termination proof uses). +pub proof fn lemma_series_end_in_range(b: Seq, i: nat, end: nat) + ensures + match series_end(b, i, end) { + Some(j) => i <= j && j < end && b[j as int] < 128, + None => true, + }, + decreases end - i, +{ + if i < end && b[i as int] >= 128 { + lemma_series_end_in_range(b, i + 1, end); + } +} + + +/// Shifting a sequence by `k` shifts where a series ends. +pub proof fn lemma_series_end_shift(b: Seq, k: nat, s: nat, e: nat) + requires + k <= e <= b.len(), + ensures + series_end(b, s + k, e) == match series_end(b.skip(k as int), s, (e - k) as nat) { + Some(j) => Some(j + k), + None => None, + }, + decreases e - k - s, +{ + if s < e - k { + assert(b.skip(k as int)[s as int] == b[(s + k) as int]); + if b[(s + k) as int] >= 128 { + lemma_series_end_shift(b, k, s + 1, e); + } + } +} + +/// One step of [`subidentifiers`] on the suffix of `c` from `i`: the first +/// series ends at `j`, and what follows is the suffix from `j + 1`. +pub proof fn lemma_subidentifiers_step(c: Seq, i: nat) + requires + i < c.len(), + ensures + subidentifiers(c.skip(i as int)) == match series_end(c, i, c.len()) { + None => None, + Some(j) => { + let v = base128(c.subrange(i as int, j as int + 1)); + if c[i as int] == 128 { + None + } else if v >= 0x1_0000_0000_0000_0000 { + None + } else { + match subidentifiers(c.skip(j as int + 1)) { + None => None, + Some(rest) => Some(seq![v] + rest), + } + } + }, + }, +{ + let rest = c.skip(i as int); + lemma_series_end_shift(c, i, 0, c.len()); + assert(rest.len() == c.len() - i); + assert(rest[0] == c[i as int]); + lemma_series_end_in_range(c, i, c.len()); + if let Some(j) = series_end(c, i, c.len()) { + let j2 = (j - i) as nat; + assert(series_end(rest, 0, rest.len()) == Some(j2)); + assert(rest.subrange(0, j2 as int + 1) =~= c.subrange(i as int, j as int + 1)); + assert(rest.skip(j2 as int + 1) =~= c.skip(j as int + 1)); + } +} + +/// An empty suffix has no subidentifiers. +pub proof fn lemma_subidentifiers_empty(c: Seq) + requires + c.len() == 0, + ensures + subidentifiers(c) == Some(Seq::::empty()), +{ +} + +// --------------------------------------------------------------------------- +// Where a reader stops. +// --------------------------------------------------------------------------- + +/// The identifier octets that were read are at least one, lie inside +/// `[pos, end)`, and the tag number is one the decoder accepts. +pub proof fn lemma_read_identifier_bounds(b: Seq, pos: nat, end: nat) + ensures + match read_identifier(b, pos, end) { + Some((tag, n)) => n >= 1 && pos + n <= end && tag.number <= MAX_TAG_NUMBER, + None => true, + }, +{ + if pos < end && b[pos as int] % 32 == 31 { + lemma_series_end_in_range(b, pos + 1, end); + } +} + +/// The length octets that were read are at least one and lie inside +/// `[pos, end)`. +pub proof fn lemma_read_length_bounds(b: Seq, pos: nat, end: nat) + ensures + match read_length(b, pos, end) { + Some((v, n)) => n >= 1 && pos + n <= end, + None => true, + }, +{ +} + +/// A value read starts after `pos`, its contents lie inside `[pos, end)`, +/// and its tag number is one the decoder accepts. +pub proof fn lemma_read_tlv_bounds(b: Seq, pos: nat, end: nat) + ensures + match read_tlv(b, pos, end) { + Some(t) => pos < t.cstart && t.cstart <= t.cend && t.cend <= end && t.tag.number <= MAX_TAG_NUMBER, + None => true, + }, +{ + lemma_read_identifier_bounds(b, pos, end); + if let Some((tag, id_len)) = read_identifier(b, pos, end) { + lemma_read_length_bounds(b, pos + id_len, end); + } +} + + +// --------------------------------------------------------------------------- +// The sizes of what the encoder writes. +// --------------------------------------------------------------------------- + +/// The identifier octets of an acceptable tag are one to three. +pub proof fn lemma_encode_identifier_len(tag: Tag) + requires + tag.number <= MAX_TAG_NUMBER, + ensures + 1 <= encode_identifier(tag).len() <= 3, +{ +} + +/// The length octets of a length below 2^64 are one to nine. +pub proof fn lemma_encode_length_len(len: nat) + requires + len < pow256(8), + ensures + 1 <= encode_length(len).len() <= 9, +{ + lemma_pow256_8(); + if len >= 128 { + lemma_be_bytes_len(len, length_octets(len)); + } +} + +/// The encoding of a value is its contents and at most 12 octets more: the +/// encoder never writes more than it was given plus a header. +pub proof fn lemma_encode_tlv_len(tag: Tag, contents: Seq) + requires + tlv_ok(tag, contents), + ensures + encode_tlv(tag, contents).len() <= contents.len() + 12, +{ + lemma_encode_identifier_len(tag); + lemma_encode_length_len(contents.len()); +} + +// --------------------------------------------------------------------------- +// Subidentifiers. +// --------------------------------------------------------------------------- + +/// `encode_subidentifiers` of a concatenation is the concatenation of the +/// `encode_subidentifiers`. +pub proof fn lemma_encode_subidentifiers_append(a: Seq, c: Seq) + ensures + encode_subidentifiers(a + c) =~= encode_subidentifiers(a) + encode_subidentifiers(c), + decreases a.len(), +{ + if a.len() == 0 { + assert(a + c =~= c); + } else { + lemma_encode_subidentifiers_append(a.drop_first(), c); + assert((a + c).drop_first() =~= a.drop_first() + c); + assert((a + c)[0] == a[0]); + assert(encode_subidentifier(a[0]) + (encode_subidentifiers(a.drop_first()) + encode_subidentifiers(c)) + =~= (encode_subidentifier(a[0]) + encode_subidentifiers(a.drop_first())) + encode_subidentifiers(c)); + } +} + +/// One subidentifier's encoding. +pub proof fn lemma_encode_subidentifiers_single(v: nat) + ensures + encode_subidentifiers(seq![v]) =~= encode_subidentifier(v), +{ + let e = Seq::::empty(); + assert(seq![v].len() == 1); + assert(seq![v][0] == v); + assert(seq![v].drop_first() =~= e); + assert(encode_subidentifiers(e) =~= Seq::::empty()); + assert(encode_subidentifier(v) + Seq::::empty() =~= encode_subidentifier(v)); +} + +} // verus! diff --git a/crates/clazel-der/src/lemmas_oid.rs b/crates/clazel-der/src/lemmas_oid.rs new file mode 100644 index 0000000..3c63ada --- /dev/null +++ b/crates/clazel-der/src/lemmas_oid.rs @@ -0,0 +1,423 @@ +//! Facts proven about OBJECT IDENTIFIER contents in the spec +//! ([`super::spec`]): the subidentifiers' encoding is the fewest octets +//! (§8.19.2), reading inverts writing and writing inverts reading (so the +//! decoder accepts exactly the encodings the encoder writes), and the +//! decoder never produces more components than there are octets. Evidence +//! that the transcription is right, not part of what the implementation must +//! meet. + +#[allow(unused_imports)] +use super::spec::*; +#[allow(unused_imports)] +use vstd::prelude::*; + +verus! { + +// --------------------------------------------------------------------------- +// One subidentifier. +// --------------------------------------------------------------------------- + +/// Every octet of a subidentifier prefix has bit 8 set. +pub proof fn lemma_prefix_all_continue(w: nat) + ensures + forall|k: int| 0 <= k < subidentifier_prefix(w).len() ==> #[trigger] subidentifier_prefix(w)[k] >= 128, + decreases w, +{ + if w > 0 { + lemma_prefix_all_continue(w / 128); + let p = subidentifier_prefix(w / 128); + let s = p + seq![(128 + w % 128) as u8]; + assert(subidentifier_prefix(w) =~= s); + assert forall|k: int| 0 <= k < s.len() implies #[trigger] s[k] >= 128 by { + if k < p.len() { + assert(s[k] == p[k]); + } + } + } +} + +/// The prefix of a positive number has a first octet above 128: its leading +/// digit is not zero (no `0x80` padding). +pub proof fn lemma_prefix_first(w: nat) + requires + w > 0, + ensures + subidentifier_prefix(w).len() >= 1, + subidentifier_prefix(w)[0] > 128, + decreases w, +{ + let p = subidentifier_prefix(w / 128); + if w / 128 > 0 { + lemma_prefix_first(w / 128); + assert(subidentifier_prefix(w)[0] == p[0]); + } else { + assert(p =~= Seq::::empty()); + assert(w % 128 == w); + assert(subidentifier_prefix(w) =~= seq![(128 + w % 128) as u8]); + } +} + +/// The prefix spells the number it was made from. +pub proof fn lemma_prefix_base128(w: nat) + ensures + base128(subidentifier_prefix(w)) == w, + decreases w, +{ + if w > 0 { + lemma_prefix_base128(w / 128); + let p = subidentifier_prefix(w / 128); + let x = (128 + w % 128) as u8; + assert(subidentifier_prefix(w) =~= p.push(x)); + super::lemmas::lemma_base128_push(p, x); + assert(x % 128 == w % 128); + } else { + assert(subidentifier_prefix(0) =~= Seq::::empty()); + } +} + +/// A subidentifier's encoding: one or more octets, all but the last with +/// bit 8 set, the last with it clear, the first not `0x80` (§8.19.2: "the +/// leading octet of the subidentifier shall not have the value 80"), and it +/// spells the subidentifier. +pub proof fn lemma_encode_subidentifier_props(v: nat) + ensures + ({ + let e = encode_subidentifier(v); + &&& e.len() >= 1 + &&& e.last() < 128 + &&& forall|k: int| 0 <= k < e.len() - 1 ==> #[trigger] e[k] >= 128 + &&& e[0] != 128 + &&& base128(e) == v + }), +{ + let p = subidentifier_prefix(v / 128); + let e = encode_subidentifier(v); + lemma_prefix_all_continue(v / 128); + lemma_prefix_base128(v / 128); + assert(e =~= p.push((v % 128) as u8)); + super::lemmas::lemma_base128_push(p, (v % 128) as u8); + assert(e.last() == (v % 128) as u8); + assert forall|k: int| 0 <= k < e.len() - 1 implies #[trigger] e[k] >= 128 by { + assert(e[k] == p[k]); + } + if v / 128 > 0 { + lemma_prefix_first(v / 128); + assert(e[0] == p[0]); + } else { + assert(p =~= Seq::::empty()); + assert(e[0] == (v % 128) as u8); + } +} + +/// A series that ends: every octet before the end has bit 8 set and the +/// end's is clear. +pub proof fn lemma_series_end_props(b: Seq, i: nat, end: nat) + ensures + match series_end(b, i, end) { + Some(j) => { + &&& i <= j < end + &&& b[j as int] < 128 + &&& forall|k: int| i <= k < j ==> #[trigger] b[k] >= 128 + }, + None => true, + }, + decreases end - i, +{ + if i < end && b[i as int] >= 128 { + lemma_series_end_props(b, i + 1, end); + } +} + +/// A run of octets whose low seven bits do not all vanish in the first +/// spells a positive number. +pub proof fn lemma_base128_positive(r: Seq) + requires + r.len() >= 1, + r[0] % 128 != 0, + ensures + base128(r) > 0, + decreases r.len(), +{ + let d = r.drop_last(); + if r.len() == 1 { + assert(d =~= Seq::::empty()); + assert(r.last() == r[0]); + } else { + assert(d[0] == r[0]); + lemma_base128_positive(d); + } +} + +/// A prefix is rebuilt from the number its octets spell: the digits are the +/// only octets of the run (all with bit 8 set, no leading `0x80`). +pub proof fn lemma_prefix_canonical(r: Seq) + requires + forall|k: int| 0 <= k < r.len() ==> #[trigger] r[k] >= 128, + r.len() >= 1 ==> r[0] != 128, + ensures + subidentifier_prefix(base128(r)) =~= r, + decreases r.len(), +{ + if r.len() > 0 { + let d = r.drop_last(); + let x = r.last(); + assert forall|k: int| 0 <= k < d.len() implies #[trigger] d[k] >= 128 by { + assert(d[k] == r[k]); + } + if d.len() >= 1 { + assert(d[0] == r[0]); + } + lemma_prefix_canonical(d); + assert(r[0] % 128 != 0); + lemma_base128_positive(r); + let w = base128(r); + assert(w == base128(d) * 128 + (x % 128) as nat); + assert(w / 128 == base128(d)) by (nonlinear_arith) + requires + w == base128(d) * 128 + (x % 128) as nat, + x % 128 < 128, + ; + assert(w % 128 == (x % 128) as nat) by (nonlinear_arith) + requires + w == base128(d) * 128 + (x % 128) as nat, + x % 128 < 128, + ; + assert(x >= 128); + assert((128 + w % 128) as u8 == x); + assert(subidentifier_prefix(w) =~= subidentifier_prefix(w / 128) + seq![x]); + assert(d + seq![x] =~= r); + } +} + +/// A series of octets (all but the last with bit 8 set, the last with it +/// clear, the first not `0x80`) is the encoding of the number it spells. +pub proof fn lemma_series_canonical(run: Seq) + requires + run.len() >= 1, + run.last() < 128, + forall|k: int| 0 <= k < run.len() - 1 ==> #[trigger] run[k] >= 128, + run[0] != 128, + ensures + encode_subidentifier(base128(run)) =~= run, +{ + let d = run.drop_last(); + let last = run.last(); + assert forall|k: int| 0 <= k < d.len() implies #[trigger] d[k] >= 128 by { + assert(d[k] == run[k]); + } + if d.len() >= 1 { + assert(d[0] == run[0]); + } + lemma_prefix_canonical(d); + let v = base128(run); + assert(v == base128(d) * 128 + (last % 128) as nat); + assert(last % 128 == last); + assert(v / 128 == base128(d)) by (nonlinear_arith) + requires + v == base128(d) * 128 + (last % 128) as nat, + last % 128 < 128, + ; + assert(v % 128 == (last % 128) as nat) by (nonlinear_arith) + requires + v == base128(d) * 128 + (last % 128) as nat, + last % 128 < 128, + ; + assert(v % 128 == last as nat); + assert(encode_subidentifier(v) =~= subidentifier_prefix(base128(d)) + seq![last]); + assert(d + seq![last] =~= run); +} + +// --------------------------------------------------------------------------- +// A list of subidentifiers. +// --------------------------------------------------------------------------- + +/// The bytes of `subidentifiers` split into the first subidentifier and the +/// rest. +proof fn lemma_series_end_of_encoding(v: nat, rest: Seq) + ensures + ({ + let e = encode_subidentifier(v); + let c = e + rest; + series_end(c, 0, c.len()) == Some((e.len() - 1) as nat) + }), +{ + lemma_encode_subidentifier_props(v); + let e = encode_subidentifier(v); + let c = e + rest; + lemma_series_end_from(c, 0, e.len()); +} + +/// A series that has no end before octet `k - 1`, and ends there, ends +/// there in any longer sequence. +proof fn lemma_series_end_from(c: Seq, i: nat, k: nat) + requires + 1 <= k <= c.len(), + i <= k - 1, + forall|m: int| i <= m < k - 1 ==> #[trigger] c[m] >= 128, + c[k - 1] < 128, + ensures + series_end(c, i, c.len()) == Some((k - 1) as nat), + decreases k - i, +{ + if i < k - 1 { + lemma_series_end_from(c, i + 1, k); + } +} + +/// Reading what [`encode_subidentifiers`] wrote: every list of +/// subidentifiers below 2^64 survives. +pub proof fn lemma_subidentifiers_encode(s: Seq) + requires + forall|i: int| 0 <= i < s.len() ==> #[trigger] s[i] < 0x1_0000_0000_0000_0000, + ensures + subidentifiers(encode_subidentifiers(s)) == Some(s), + decreases s.len(), +{ + let c = encode_subidentifiers(s); + if s.len() == 0 { + assert(c =~= Seq::::empty()); + } else { + let rest = s.drop_first(); + let v = s[0]; + let e = encode_subidentifier(v); + let r = encode_subidentifiers(rest); + assert(c == e + r); + lemma_encode_subidentifier_props(v); + assert forall|i: int| 0 <= i < rest.len() implies #[trigger] rest[i] < 0x1_0000_0000_0000_0000 by { + assert(rest[i] == s[i + 1]); + } + lemma_subidentifiers_encode(rest); + lemma_series_end_of_encoding(v, r); + let j = (e.len() - 1) as nat; + assert(c.len() > 0); + assert(c.subrange(0, j as int + 1) =~= e); + assert(c[0] == e[0]); + assert(c.skip(j as int + 1) =~= r); + assert(seq![v] + rest =~= s); + } +} + +/// Writing what [`subidentifiers`] read: the octets are the encoding of the +/// subidentifiers that were read, and each is below 2^64 (the decoder's +/// limit). +pub proof fn lemma_encode_subidentifiers_decode(c: Seq) + ensures + match subidentifiers(c) { + Some(s) => { + &&& encode_subidentifiers(s) =~= c + &&& forall|i: int| 0 <= i < s.len() ==> #[trigger] s[i] < 0x1_0000_0000_0000_0000 + &&& s.len() <= c.len() + }, + None => true, + }, + decreases c.len(), +{ + if c.len() > 0 { + lemma_series_end_props(c, 0, c.len()); + if let Some(j) = series_end(c, 0, c.len()) { + let run = c.subrange(0, j as int + 1); + let v = base128(run); + if c[0] != 128 && v < 0x1_0000_0000_0000_0000 { + let rest_bytes = c.skip(j as int + 1); + lemma_encode_subidentifiers_decode(rest_bytes); + if let Some(rest) = subidentifiers(rest_bytes) { + assert(run.last() == c[j as int]); + assert forall|k: int| 0 <= k < run.len() - 1 implies #[trigger] run[k] >= 128 by { + assert(run[k] == c[k]); + } + assert(run[0] == c[0]); + lemma_series_canonical(run); + let s = seq![v] + rest; + assert(s.drop_first() =~= rest); + assert(s[0] == v); + assert(encode_subidentifiers(s) == encode_subidentifier(v) + encode_subidentifiers(rest)); + assert(run + rest_bytes =~= c); + assert forall|i: int| 0 <= i < s.len() implies #[trigger] s[i] < 0x1_0000_0000_0000_0000 by { + if i > 0 { + assert(s[i] == rest[i - 1]); + } + } + } + } + } + } +} + +// --------------------------------------------------------------------------- +// Components. +// --------------------------------------------------------------------------- + +/// The first two components survive being packed into the first +/// subidentifier and unpacked. +pub proof fn lemma_first_components(x: nat, y: nat) + requires + x <= 2, + x < 2 ==> y < 40, + ensures + first_components(40 * x + y) == (x, y), +{ +} + +/// Reading what [`oid_contents`] wrote: every valid list of components +/// survives. +pub proof fn lemma_oid_components_oid_contents(arcs: Seq) + requires + oid_valid(arcs), + ensures + oid_components(oid_contents(arcs)) == Some(arcs), +{ + let subs = seq![40 * arcs[0] + arcs[1]] + arcs.skip(2); + assert forall|i: int| 0 <= i < subs.len() implies #[trigger] subs[i] < 0x1_0000_0000_0000_0000 by { + if i > 0 { + assert(subs[i] == arcs[i + 1]); + } + } + lemma_subidentifiers_encode(subs); + lemma_first_components(arcs[0], arcs[1]); + assert(subs[0] == 40 * arcs[0] + arcs[1]); + assert(subs.skip(1) =~= arcs.skip(2)); + assert(seq![arcs[0], arcs[1]] + arcs.skip(2) =~= arcs); +} + +/// Writing what [`oid_components`] read: the octets are the encoding of the +/// components that were read, which are valid, and there are no more of +/// them than the octets plus one (the first subidentifier gives two). +pub proof fn lemma_oid_contents_oid_components(c: Seq) + ensures + match oid_components(c) { + Some(arcs) => oid_valid(arcs) && oid_contents(arcs) =~= c && arcs.len() <= c.len() + 1, + None => true, + }, +{ + lemma_encode_subidentifiers_decode(c); + if let Some(subs) = subidentifiers(c) { + if subs.len() > 0 { + let (x, y) = first_components(subs[0]); + let arcs = seq![x, y] + subs.skip(1); + assert(arcs[0] == x); + assert(arcs[1] == y); + assert(arcs.skip(2) =~= subs.skip(1)); + assert(40 * x + y == subs[0]); + assert(subs[0] < 0x1_0000_0000_0000_0000); + assert forall|i: int| 2 <= i < arcs.len() implies arcs[i] < 0x1_0000_0000_0000_0000 by { + assert(arcs[i] == subs[i - 1]); + } + assert(seq![40 * x + y] + subs.skip(1) =~= subs); + } + } +} + +/// The number of components decoded never exceeds the octets (plus one): +/// a vector built from them holds no more than there are octets to have +/// read, so a declared length of 2^64 reserves nothing. +pub proof fn lemma_oid_components_bound(c: Seq) + ensures + match oid_components(c) { + Some(arcs) => arcs.len() <= c.len() + 1, + None => true, + }, +{ + lemma_oid_contents_oid_components(c); +} + +} // verus! diff --git a/crates/clazel-der/src/lemmas_overread.rs b/crates/clazel-der/src/lemmas_overread.rs new file mode 100644 index 0000000..88e1ef7 --- /dev/null +++ b/crates/clazel-der/src/lemmas_overread.rs @@ -0,0 +1,283 @@ +//! No over-read: every reader consults only the bytes of the value it +//! reads, `[pos, cend)`, and never a byte at or after `end`. Stated as: if a +//! reader succeeds on `b1`, then it succeeds with the same result on any +//! other input `b2` (and any other end) that agrees with `b1` on the bytes +//! the value occupies. The bytes after the value, and after `end`, are +//! free. Evidence that the transcription is right, not part of what the +//! implementation must meet. + +#[allow(unused_imports)] +use super::spec::*; +#[allow(unused_imports)] +use vstd::prelude::*; + +verus! { + +/// `b1` and `b2` hold the same octets at every index in `[lo, hi)`. +pub open spec fn agree(b1: Seq, b2: Seq, lo: nat, hi: nat) -> bool { + forall|k: int| lo <= k < hi ==> #[trigger] b1[k] == #[trigger] b2[k] +} + +/// A series that ends at `j` in `b1` ends there in `b2`, if the two agree +/// up to `j` and `j` is before the second input's end. +pub proof fn lemma_series_end_agree(b1: Seq, b2: Seq, i: nat, e1: nat, e2: nat, j: nat) + requires + series_end(b1, i, e1) == Some(j), + j < e2, + agree(b1, b2, i, j + 1), + ensures + series_end(b2, i, e2) == Some(j), + decreases j - i, +{ + super::lemmas::lemma_series_end_in_range(b1, i, e1); + if i < j { + assert(b1[i as int] == b2[i as int]); + lemma_series_end_agree(b1, b2, i + 1, e1, e2, j); + } else { + assert(b1[i as int] == b2[i as int]); + } +} + +/// An identifier read from `b1` reads the same from `b2` when the two agree +/// on the identifier octets. +pub proof fn lemma_read_identifier_no_overread(b1: Seq, b2: Seq, pos: nat, e1: nat, e2: nat) + requires + e1 <= b1.len(), + e2 <= b2.len(), + read_identifier(b1, pos, e1) is Some, + pos + read_identifier(b1, pos, e1).unwrap().1 <= e2, + agree(b1, b2, pos, pos + read_identifier(b1, pos, e1).unwrap().1), + ensures + read_identifier(b2, pos, e2) == read_identifier(b1, pos, e1), +{ + super::lemmas::lemma_read_identifier_bounds(b1, pos, e1); + let (tag, n) = read_identifier(b1, pos, e1).unwrap(); + assert(b1[pos as int] == b2[pos as int]); + if b1[pos as int] % 32 == 31 { + super::lemmas::lemma_series_end_in_range(b1, pos + 1, e1); + let j = series_end(b1, pos + 1, e1).unwrap(); + assert(j + 1 == pos + n); + lemma_series_end_agree(b1, b2, pos + 1, e1, e2, j); + assert(b1.subrange(pos as int + 1, j as int + 1) =~= b2.subrange(pos as int + 1, j as int + 1)); + assert(b1[pos as int + 1] == b2[pos as int + 1]); + } +} + +/// A length read from `b1` reads the same from `b2` when the two agree on +/// the length octets. +pub proof fn lemma_read_length_no_overread(b1: Seq, b2: Seq, pos: nat, e1: nat, e2: nat) + requires + e1 <= b1.len(), + e2 <= b2.len(), + read_length(b1, pos, e1) is Some, + pos + read_length(b1, pos, e1).unwrap().1 <= e2, + agree(b1, b2, pos, pos + read_length(b1, pos, e1).unwrap().1), + ensures + read_length(b2, pos, e2) == read_length(b1, pos, e1), +{ + super::lemmas::lemma_read_length_bounds(b1, pos, e1); + let (v, n) = read_length(b1, pos, e1).unwrap(); + assert(b1[pos as int] == b2[pos as int]); + if b1[pos as int] >= 128 { + let k = (b1[pos as int] - 128) as nat; + assert(n == 1 + k); + assert(b1[pos as int + 1] == b2[pos as int + 1]); + assert(b1.subrange(pos as int + 1, pos as int + 1 + k as int) =~= b2.subrange(pos as int + 1, pos as int + 1 + k as int)); + } +} + +/// A value read from `b1` reads the same from `b2` when the two agree on +/// the bytes the value occupies, `[pos, cend)`: the decoder consults no +/// byte after the length it read, and none at or after `end`. +pub proof fn lemma_read_tlv_no_overread(b1: Seq, b2: Seq, pos: nat, e1: nat, e2: nat) + requires + e1 <= b1.len(), + e2 <= b2.len(), + read_tlv(b1, pos, e1) is Some, + read_tlv(b1, pos, e1).unwrap().cend <= e2, + agree(b1, b2, pos, read_tlv(b1, pos, e1).unwrap().cend), + ensures + read_tlv(b2, pos, e2) == read_tlv(b1, pos, e1), +{ + super::lemmas::lemma_read_tlv_bounds(b1, pos, e1); + let t = read_tlv(b1, pos, e1).unwrap(); + super::lemmas::lemma_read_identifier_bounds(b1, pos, e1); + let (tag, id_len) = read_identifier(b1, pos, e1).unwrap(); + lemma_read_identifier_no_overread(b1, b2, pos, e1, e2); + super::lemmas::lemma_read_length_bounds(b1, pos + id_len, e1); + let (len, len_len) = read_length(b1, pos + id_len, e1).unwrap(); + lemma_read_length_no_overread(b1, b2, pos + id_len, e1, e2); +} + +/// The result of reading a value depends only on the bytes in `[pos, +/// end)`: two inputs that agree there give the same result, success or +/// failure. +pub proof fn lemma_read_tlv_within_end(b1: Seq, b2: Seq, pos: nat, end: nat) + requires + end <= b1.len(), + end <= b2.len(), + agree(b1, b2, pos, end), + ensures + read_tlv(b2, pos, end) == read_tlv(b1, pos, end), +{ + super::lemmas::lemma_read_tlv_bounds(b1, pos, end); + super::lemmas::lemma_read_tlv_bounds(b2, pos, end); + if read_tlv(b1, pos, end) is Some { + lemma_read_tlv_no_overread(b1, b2, pos, end, end); + } else if read_tlv(b2, pos, end) is Some { + assert(forall|k: int| pos <= k < end ==> b2[k] == b1[k]); + lemma_read_tlv_no_overread(b2, b1, pos, end, end); + } +} + +/// The contents of a value read from `b1` are the same octets in `b2`. +pub proof fn lemma_contents_agree(b1: Seq, b2: Seq, cstart: nat, cend: nat) + requires + cstart <= cend <= b1.len(), + cend <= b2.len(), + agree(b1, b2, cstart, cend), + ensures + b1.subrange(cstart as int, cend as int) =~= b2.subrange(cstart as int, cend as int), +{ +} + +/// A BOOLEAN read from `b1` reads the same from `b2` when the two agree on +/// the octets it occupies. +pub proof fn lemma_read_boolean_no_overread(b1: Seq, b2: Seq, pos: nat, e1: nat, e2: nat) + requires + e1 <= b1.len(), + e2 <= b2.len(), + read_boolean(b1, pos, e1) is Some, + read_boolean(b1, pos, e1).unwrap().1 <= e2, + agree(b1, b2, pos, read_boolean(b1, pos, e1).unwrap().1), + ensures + read_boolean(b2, pos, e2) == read_boolean(b1, pos, e1), +{ + super::lemmas::lemma_read_tlv_bounds(b1, pos, e1); + let t = read_tlv(b1, pos, e1).unwrap(); + lemma_read_tlv_no_overread(b1, b2, pos, e1, e2); + lemma_contents_agree(b1, b2, t.cstart, t.cend); +} + +/// An INTEGER read from `b1` reads the same from `b2` when the two agree on +/// the octets it occupies. +pub proof fn lemma_read_integer_no_overread(b1: Seq, b2: Seq, pos: nat, e1: nat, e2: nat) + requires + e1 <= b1.len(), + e2 <= b2.len(), + read_integer(b1, pos, e1) is Some, + read_integer(b1, pos, e1).unwrap().3 <= e2, + agree(b1, b2, pos, read_integer(b1, pos, e1).unwrap().3), + ensures + read_integer(b2, pos, e2) == read_integer(b1, pos, e1), +{ + super::lemmas::lemma_read_tlv_bounds(b1, pos, e1); + let t = read_tlv(b1, pos, e1).unwrap(); + lemma_read_tlv_no_overread(b1, b2, pos, e1, e2); + lemma_contents_agree(b1, b2, t.cstart, t.cend); +} + +/// A BIT STRING read from `b1` reads the same from `b2` when the two agree +/// on the octets it occupies. +pub proof fn lemma_read_bit_string_no_overread(b1: Seq, b2: Seq, pos: nat, e1: nat, e2: nat) + requires + e1 <= b1.len(), + e2 <= b2.len(), + read_bit_string(b1, pos, e1) is Some, + read_bit_string(b1, pos, e1).unwrap().3 <= e2, + agree(b1, b2, pos, read_bit_string(b1, pos, e1).unwrap().3), + ensures + read_bit_string(b2, pos, e2) == read_bit_string(b1, pos, e1), +{ + super::lemmas::lemma_read_tlv_bounds(b1, pos, e1); + let t = read_tlv(b1, pos, e1).unwrap(); + lemma_read_tlv_no_overread(b1, b2, pos, e1, e2); + lemma_contents_agree(b1, b2, t.cstart, t.cend); +} + +/// A string (or OCTET STRING) read from `b1` reads the same from `b2` when +/// the two agree on the octets it occupies. +pub proof fn lemma_read_octets_no_overread(b1: Seq, b2: Seq, pos: nat, e1: nat, e2: nat, number: u32) + requires + e1 <= b1.len(), + e2 <= b2.len(), + read_octets(b1, pos, e1, number) is Some, + read_octets(b1, pos, e1, number).unwrap().1 <= e2, + agree(b1, b2, pos, read_octets(b1, pos, e1, number).unwrap().1), + ensures + read_octets(b2, pos, e2, number) == read_octets(b1, pos, e1, number), +{ + super::lemmas::lemma_read_tlv_bounds(b1, pos, e1); + lemma_read_tlv_no_overread(b1, b2, pos, e1, e2); +} + +/// A NULL read from `b1` reads the same from `b2` when the two agree on the +/// octets it occupies. +pub proof fn lemma_read_null_no_overread(b1: Seq, b2: Seq, pos: nat, e1: nat, e2: nat) + requires + e1 <= b1.len(), + e2 <= b2.len(), + read_null(b1, pos, e1) is Some, + read_null(b1, pos, e1).unwrap() <= e2, + agree(b1, b2, pos, read_null(b1, pos, e1).unwrap()), + ensures + read_null(b2, pos, e2) == read_null(b1, pos, e1), +{ + super::lemmas::lemma_read_tlv_bounds(b1, pos, e1); + lemma_read_tlv_no_overread(b1, b2, pos, e1, e2); +} + +/// An OBJECT IDENTIFIER read from `b1` reads the same from `b2` when the +/// two agree on the octets it occupies. +pub proof fn lemma_read_oid_no_overread(b1: Seq, b2: Seq, pos: nat, e1: nat, e2: nat) + requires + e1 <= b1.len(), + e2 <= b2.len(), + read_oid(b1, pos, e1) is Some, + read_oid(b1, pos, e1).unwrap().1 <= e2, + agree(b1, b2, pos, read_oid(b1, pos, e1).unwrap().1), + ensures + read_oid(b2, pos, e2) == read_oid(b1, pos, e1), +{ + super::lemmas::lemma_read_tlv_bounds(b1, pos, e1); + let t = read_tlv(b1, pos, e1).unwrap(); + lemma_read_tlv_no_overread(b1, b2, pos, e1, e2); + lemma_contents_agree(b1, b2, t.cstart, t.cend); +} + +/// A SEQUENCE or SET (or any universal constructed value) read from `b1` +/// reads the same from `b2` when the two agree on the octets it occupies. +pub proof fn lemma_expect_constructed_no_overread(b1: Seq, b2: Seq, pos: nat, e1: nat, e2: nat, number: u32) + requires + e1 <= b1.len(), + e2 <= b2.len(), + expect_constructed(b1, pos, e1, number) is Some, + expect_constructed(b1, pos, e1, number).unwrap().1 <= e2, + agree(b1, b2, pos, expect_constructed(b1, pos, e1, number).unwrap().1), + ensures + expect_constructed(b2, pos, e2, number) == expect_constructed(b1, pos, e1, number), +{ + super::lemmas::lemma_read_tlv_bounds(b1, pos, e1); + lemma_read_tlv_no_overread(b1, b2, pos, e1, e2); +} + +/// An OPTIONAL context-specific component that is present in `b1` is +/// present, the same, in `b2` when the two agree on the octets it occupies. +pub proof fn lemma_read_optional_context_no_overread(b1: Seq, b2: Seq, pos: nat, e1: nat, e2: nat, number: u32) + requires + e1 <= b1.len(), + e2 <= b2.len(), + read_optional_context(b1, pos, e1, number) matches Some(Some(_)), + read_optional_context(b1, pos, e1, number).unwrap().unwrap().2 <= e2, + agree(b1, b2, pos, read_optional_context(b1, pos, e1, number).unwrap().unwrap().2), + ensures + read_optional_context(b2, pos, e2, number) == read_optional_context(b1, pos, e1, number), +{ + super::lemmas::lemma_read_tlv_bounds(b1, pos, e1); + super::lemmas::lemma_read_identifier_bounds(b1, pos, e1); + let t = read_tlv(b1, pos, e1).unwrap(); + lemma_read_tlv_no_overread(b1, b2, pos, e1, e2); + lemma_read_identifier_no_overread(b1, b2, pos, e1, e2); +} + +} // verus! diff --git a/crates/clazel-der/src/lemmas_tlv.rs b/crates/clazel-der/src/lemmas_tlv.rs new file mode 100644 index 0000000..16f1067 --- /dev/null +++ b/crates/clazel-der/src/lemmas_tlv.rs @@ -0,0 +1,526 @@ +//! Facts proven about the identifier, length and value framing of the spec +//! ([`super::spec`]): the round trip (`read_*` inverts `encode_*`), DER +//! canonicity (`encode_*` inverts `read_*`, so the encoding of a value is +//! the one the decoder accepted) and the shortest length form. They are +//! evidence that the transcription is right, not part of what the +//! implementation must meet. + +#[allow(unused_imports)] +use super::spec::*; +#[allow(unused_imports)] +use vstd::prelude::*; + +verus! { + +// --------------------------------------------------------------------------- +// Arithmetic on octets. +// --------------------------------------------------------------------------- + +/// The first octet of an identifier is its class bits, its form bit and the +/// low five bits: reading them back recovers each. +pub proof fn lemma_first_octet(cb: u8, constructed: bool, low: u8) + requires + cb <= 3, + low <= 31, + ensures + ({ + let o = (cb * 64 + (if constructed { 32u8 } else { 0u8 }) + low) as u8; + &&& o / 64 == cb + &&& ((o / 32) % 2 == 1) == constructed + &&& o % 32 == low + }), +{ + if constructed { + assert((cb * 64 + 32 + low) / 64 == cb); + assert(((cb * 64 + 32 + low) / 32) % 2 == 1); + assert((cb * 64 + 32 + low) % 32 == low); + } else { + assert((cb * 64 + low) / 64 == cb); + assert(((cb * 64 + low) / 32) % 2 == 0); + assert((cb * 64 + low) % 32 == low); + } +} + +/// An octet is its class bits, form bit and low five bits. +pub proof fn lemma_octet_split(o: u8) + ensures + o == (o / 64) * 64 + (if (o / 32) % 2 == 1 { 32u8 } else { 0u8 }) + o % 32, +{ +} + +/// The class survives a trip through its bits. +pub proof fn lemma_class_bits_roundtrip(class: Class) + ensures + class_bits(class) <= 3, + class_from_bits(class_bits(class)) == class, +{ +} + +/// The class bits of an octet name its class, which encodes back to them. +pub proof fn lemma_class_from_bits_roundtrip(bits: u8) + requires + bits <= 3, + ensures + class_bits(class_from_bits(bits)) == bits, +{ +} + +/// 256^n grows with n. +pub proof fn lemma_pow256_monotone(a: nat, b: nat) + requires + a <= b, + ensures + pow256(a) <= pow256(b), + decreases b - a, +{ + if a < b { + lemma_pow256_monotone(a, (b - 1) as nat); + super::lemmas::lemma_pow256_positive((b - 1) as nat); + } +} + +/// A number below 256^w whose spelling's first octet is zero is below +/// 256^(w-1), and so a spelling with a nonzero first octet is at least +/// 256^(w-1). +pub proof fn lemma_be_value_leading_zero(s: Seq) + requires + s.len() >= 1, + s[0] == 0, + ensures + be_value(s) < pow256((s.len() - 1) as nat), + decreases s.len(), +{ + let d = s.drop_last(); + if s.len() == 1 { + assert(d =~= Seq::::empty()); + assert(be_value(d) == 0); + assert(s.last() == s[0]); + assert(be_value(s) == 0); + assert(pow256(0) == 1); + } else { + assert(d[0] == 0); + lemma_be_value_leading_zero(d); + assert(pow256((s.len() - 1) as nat) == 256 * pow256((s.len() - 2) as nat)); + assert(be_value(s) == be_value(d) * 256 + s.last() as nat); + } +} + +/// A spelling whose first octet is not zero is at least 256^(w-1). +pub proof fn lemma_be_value_leading_nonzero(s: Seq) + requires + s.len() >= 1, + s[0] != 0, + ensures + be_value(s) >= pow256((s.len() - 1) as nat), + decreases s.len(), +{ + if s.len() == 1 { + assert(s.drop_last() =~= Seq::::empty()); + assert(pow256(0) == 1); + } else { + let d = s.drop_last(); + assert(d[0] == s[0]); + lemma_be_value_leading_nonzero(d); + assert(pow256((s.len() - 1) as nat) == 256 * pow256((s.len() - 2) as nat)); + assert(be_value(s) == be_value(d) * 256 + s.last() as nat); + } +} + +/// The first octet of `be_bytes(v, w)` is nonzero when `v` is at least +/// 256^(w-1) (and below 256^w). +pub proof fn lemma_be_bytes_first_nonzero(v: nat, w: nat) + requires + w >= 1, + v < pow256(w), + v >= pow256((w - 1) as nat), + ensures + be_bytes(v, w).len() == w, + be_bytes(v, w)[0] != 0, +{ + super::lemmas::lemma_be_bytes_len(v, w); + super::lemmas::lemma_be_value_of_be_bytes(v, w); + let s = be_bytes(v, w); + if s[0] == 0 { + lemma_be_value_leading_zero(s); + } +} + +/// A one-octet series spells its low seven bits. +pub proof fn lemma_base128_single(x: u8) + ensures + base128(seq![x]) == (x % 128) as nat, +{ + assert(seq![x].drop_last() =~= Seq::::empty()); + assert(base128(Seq::::empty()) == 0); +} + +/// A two-octet series. +pub proof fn lemma_base128_pair(x: u8, y: u8) + ensures + base128(seq![x, y]) == (x % 128) as nat * 128 + (y % 128) as nat, +{ + assert(seq![x, y].drop_last() =~= seq![x]); + lemma_base128_single(x); +} + +// --------------------------------------------------------------------------- +// Identifier octets. +// --------------------------------------------------------------------------- + +/// The identifier octets of a tag read back as that tag, wherever they sit +/// in the input and whatever follows. +pub proof fn lemma_read_identifier_encode_identifier(tag: Tag, b: Seq, pos: nat, end: nat) + requires + tag.number <= MAX_TAG_NUMBER, + pos + encode_identifier(tag).len() <= end, + end <= b.len(), + b.subrange(pos as int, (pos + encode_identifier(tag).len()) as int) =~= encode_identifier(tag), + ensures + read_identifier(b, pos, end) == Some((tag, encode_identifier(tag).len())), +{ + let enc = encode_identifier(tag); + let cb = class_bits(tag.class); + lemma_class_bits_roundtrip(tag.class); + let first = (cb * 64 + (if tag.constructed { 32u8 } else { 0u8 })) as u8; + assert(b[pos as int] == enc[0]); + if tag.number < 31 { + lemma_first_octet(cb, tag.constructed, tag.number as u8); + assert(enc[0] == (first + tag.number as u8) as u8); + } else { + lemma_first_octet(cb, tag.constructed, 31); + assert(enc[0] == (first + 31) as u8); + assert(b[pos as int + 1] == enc[1]); + if tag.number < 128 { + assert(enc =~= seq![(first + 31) as u8, tag.number as u8]); + assert(series_end(b, pos + 1, end) == Some((pos + 1) as nat)); + assert(b.subrange(pos as int + 1, pos as int + 2) =~= seq![tag.number as u8]); + lemma_base128_single(tag.number as u8); + } else { + let d1 = (128 + tag.number / 128) as u8; + let d0 = (tag.number % 128) as u8; + assert(enc =~= seq![(first + 31) as u8, d1, d0]); + assert(b[pos as int + 2] == enc[2]); + assert(d1 >= 128); + assert(b[pos as int + 1] == d1); + assert(b[pos as int + 2] == d0); + assert(d0 < 128); + assert(pos + 2 < end); + assert(series_end(b, pos + 2, end) == Some((pos + 2) as nat)); + assert(series_end(b, pos + 1, end) == Some((pos + 2) as nat)); + let s = b.subrange(pos as int + 1, pos as int + 3); + assert(s =~= seq![d1, d0]); + lemma_base128_pair(d1, d0); + assert(base128(s) == tag.number as nat); + } + } +} + +/// A run of three or more octets, the first with a nonzero low seven bits, +/// spells a number of at least 2^14. +pub proof fn lemma_base128_three_digits(b: Seq, i: int, j: int) + requires + 0 <= i, + i + 3 <= j <= b.len(), + b[i] % 128 != 0, + ensures + base128(b.subrange(i, j)) >= 16384, +{ + super::lemmas::lemma_base128_monotone(b, i, i + 3, j); + let s = b.subrange(i, i + 3); + assert(s.drop_last() =~= b.subrange(i, i + 2)); + assert(b.subrange(i, i + 2).drop_last() =~= b.subrange(i, i + 1)); + assert(b.subrange(i, i + 1).drop_last() =~= Seq::::empty()); + assert(b.subrange(i, i + 1) =~= seq![b[i]]); + lemma_base128_single(b[i]); + assert(base128(b.subrange(i, i + 2)) == base128(b.subrange(i, i + 1)) * 128 + (b[i + 1] % 128) as nat); + assert(base128(s) == base128(b.subrange(i, i + 2)) * 128 + (b[i + 2] % 128) as nat); +} + +/// The identifier octets that were read are the encoding of the tag that was +/// read: encoding the tag gives back the bytes (DER canonicity). +pub proof fn lemma_encode_identifier_read_identifier(b: Seq, pos: nat, end: nat) + requires + end <= b.len(), + ensures + match read_identifier(b, pos, end) { + Some((tag, n)) => { + &&& tag.number <= MAX_TAG_NUMBER + &&& pos + n <= end + &&& b.subrange(pos as int, (pos + n) as int) =~= encode_identifier(tag) + }, + None => true, + }, +{ + super::lemmas::lemma_read_identifier_bounds(b, pos, end); + if let Some((tag, n)) = read_identifier(b, pos, end) { + let o = b[pos as int]; + lemma_octet_split(o); + let cb = o / 64; + lemma_class_from_bits_roundtrip(cb); + let constructed = (o / 32) % 2 == 1; + let first = (cb * 64 + (if constructed { 32u8 } else { 0u8 })) as u8; + assert(tag.constructed == constructed); + assert(class_bits(tag.class) == cb); + if o % 32 != 31 { + assert(n == 1); + assert(tag.number == (o % 32) as u32); + assert(encode_identifier(tag) =~= seq![o]); + assert(b.subrange(pos as int, pos as int + 1) =~= seq![o]); + } else { + super::lemmas::lemma_series_end_in_range(b, pos + 1, end); + let j = series_end(b, pos + 1, end).unwrap(); + let run = b.subrange(pos as int + 1, j as int + 1); + assert(tag.number as nat == base128(run)); + assert(b[pos as int + 1] % 128 != 0); + assert(tag.number >= 31); + if j >= pos + 3 { + lemma_base128_three_digits(b, pos as int + 1, j as int + 1); + assert(false); + } + if j == pos + 1 { + // One subsequent octet, below 128. + assert(b[pos as int + 1] < 128); + assert(run =~= seq![b[pos as int + 1]]); + lemma_base128_single(b[pos as int + 1]); + assert(tag.number as nat == (b[pos as int + 1] % 128) as nat); + assert(tag.number < 128); + assert(encode_identifier(tag) =~= seq![o, b[pos as int + 1]]); + assert(b.subrange(pos as int, pos as int + 2) =~= seq![o, b[pos as int + 1]]); + } else { + // Two subsequent octets: the first at least 128. + assert(j == pos + 2); + let x = b[pos as int + 1]; + let y = b[pos as int + 2]; + assert(x >= 128) by { + if x < 128 { + assert(series_end(b, pos + 1, end) == Some((pos + 1) as nat)); + } + } + assert(run =~= seq![x, y]); + lemma_base128_pair(x, y); + assert(tag.number as nat == (x % 128) as nat * 128 + (y % 128) as nat); + assert(tag.number / 128 == (x % 128) as nat); + assert(tag.number % 128 == y as nat); + assert(tag.number >= 128); + assert(encode_identifier(tag) =~= seq![o, x, y]); + assert(b.subrange(pos as int, pos as int + 3) =~= seq![o, x, y]); + } + } + } +} + +// --------------------------------------------------------------------------- +// Length octets. +// --------------------------------------------------------------------------- + +/// For a length of 128 or more, the number of octets the encoder chooses is +/// the least `w` with `len < 256^w`, and the length is at least +/// 256^(w-1): the first octet of its spelling is nonzero. +pub proof fn lemma_length_octets_bounds(len: nat) + requires + 128 <= len < pow256(8), + ensures + 1 <= length_octets(len) <= 8, + len < pow256(length_octets(len)), + len >= pow256((length_octets(len) - 1) as nat), + forall|w: nat| w < length_octets(len) ==> len >= pow256(w), +{ + super::lemmas::lemma_pow256_8(); + assert forall|w: nat| w < length_octets(len) implies len >= pow256(w) by { + lemma_pow256_monotone(w, (length_octets(len) - 1) as nat); + } +} + +/// `encode_length` is the shortest form (§10.1): one octet below 128, and +/// otherwise the initial octet and the fewest octets that hold the length: +/// no smaller count of octets could. +pub proof fn lemma_encode_length_shortest(len: nat) + requires + len < pow256(8), + ensures + len < 128 ==> encode_length(len).len() == 1, + len >= 128 ==> { + let w = length_octets(len); + &&& encode_length(len).len() == 1 + w + &&& len < pow256(w) + &&& forall|w2: nat| w2 < w ==> len >= pow256(w2) + }, +{ + if len >= 128 { + lemma_length_octets_bounds(len); + super::lemmas::lemma_be_bytes_len(len, length_octets(len)); + } +} + +/// The length octets of a length below 2^64 read back as that length. +pub proof fn lemma_read_length_encode_length(len: nat, b: Seq, pos: nat, end: nat) + requires + len < pow256(8), + pos + encode_length(len).len() <= end, + end <= b.len(), + b.subrange(pos as int, (pos + encode_length(len).len()) as int) =~= encode_length(len), + ensures + read_length(b, pos, end) == Some((len, encode_length(len).len())), +{ + let enc = encode_length(len); + assert(b[pos as int] == enc[0]); + if len >= 128 { + let w = length_octets(len); + lemma_length_octets_bounds(len); + super::lemmas::lemma_be_bytes_len(len, w); + assert(enc[0] == (128 + w) as u8); + assert(pos + 1 + w <= end); + // The subsequent octets are the spelling of `len`. + let sub = b.subrange(pos as int + 1, pos as int + 1 + w as int); + assert(sub =~= be_bytes(len, w)) by { + assert forall|i: int| 0 <= i < w implies #[trigger] sub[i] == be_bytes(len, w)[i] by { + assert(b.subrange(pos as int, (pos + enc.len()) as int)[1 + i] == enc[1 + i]); + assert(enc[1 + i] == be_bytes(len, w)[i]); + } + } + lemma_be_bytes_first_nonzero(len, w); + super::lemmas::lemma_be_value_of_be_bytes(len, w); + assert(b[pos as int + 1] == be_bytes(len, w)[0]); + } +} + +/// The length octets that were read are the encoding of the length that was +/// read (DER canonicity: shortest form, no leading zero, no indefinite +/// form), and the length is below 2^64. +pub proof fn lemma_encode_length_read_length(b: Seq, pos: nat, end: nat) + requires + end <= b.len(), + ensures + match read_length(b, pos, end) { + Some((v, n)) => { + &&& v < pow256(8) + &&& pos + n <= end + &&& b.subrange(pos as int, (pos + n) as int) =~= encode_length(v) + }, + None => true, + }, +{ + super::lemmas::lemma_pow256_8(); + if let Some((v, n)) = read_length(b, pos, end) { + let f = b[pos as int]; + if f < 128 { + assert(encode_length(v) =~= seq![f]); + assert(b.subrange(pos as int, pos as int + 1) =~= seq![f]); + } else { + let k = (f - 128) as nat; + let sub = b.subrange(pos as int + 1, pos as int + 1 + k as int); + super::lemmas::lemma_be_value_bound(sub); + lemma_pow256_monotone(k, 8); + assert(sub.len() == k); + assert(b[pos as int + 1] != 0); + assert(sub[0] == b[pos as int + 1]); + lemma_be_value_leading_nonzero(sub); + super::lemmas::lemma_be_bytes_of_be_value(sub); + // `v` needs exactly `k` octets: it is below 256^k and at least + // 256^(k-1). + assert(length_octets(v) == k) by { + assert(pow256(1) == 256); + if k == 1 { + assert(v < 256); + } else { + lemma_pow256_monotone(1, (k - 1) as nat); + assert(v >= 256); + } + } + assert(encode_length(v) =~= seq![f] + be_bytes(v, k)); + assert(b.subrange(pos as int, (pos + n) as int) =~= seq![f] + sub); + } + } +} + + +// --------------------------------------------------------------------------- +// A whole value. +// --------------------------------------------------------------------------- + +/// If `a + c` sits at `pos`, then `a` sits at `pos` and `c` right after it. +pub proof fn lemma_subrange_split(b: Seq, pos: nat, a: Seq, c: Seq) + requires + pos + a.len() + c.len() <= b.len(), + b.subrange(pos as int, (pos + a.len() + c.len()) as int) =~= a + c, + ensures + b.subrange(pos as int, (pos + a.len()) as int) =~= a, + b.subrange((pos + a.len()) as int, (pos + a.len() + c.len()) as int) =~= c, +{ + let whole = b.subrange(pos as int, (pos + a.len() + c.len()) as int); + assert forall|i: int| 0 <= i < a.len() implies #[trigger] b.subrange(pos as int, (pos + a.len()) as int)[i] == a[i] by { + assert(whole[i] == (a + c)[i]); + } + assert forall|i: int| 0 <= i < c.len() implies #[trigger] b.subrange( + (pos + a.len()) as int, + (pos + a.len() + c.len()) as int, + )[i] == c[i] by { + assert(whole[a.len() + i] == (a + c)[a.len() + i]); + } +} + +/// The encoding of a value, found at `pos` in the input, reads back as that +/// value: the tag, and the contents range, wherever the encoding sits and +/// whatever follows it. +pub proof fn lemma_read_tlv_encode_tlv(tag: Tag, contents: Seq, b: Seq, pos: nat, end: nat) + requires + tlv_ok(tag, contents), + pos + encode_tlv(tag, contents).len() <= end, + end <= b.len(), + b.subrange(pos as int, (pos + encode_tlv(tag, contents).len()) as int) =~= encode_tlv(tag, contents), + ensures + ({ + let il = encode_identifier(tag).len(); + let ll = encode_length(contents.len()).len(); + &&& read_tlv(b, pos, end) == Some( + Tlv { tag, cstart: pos + il + ll, cend: pos + il + ll + contents.len() }, + ) + &&& b.subrange((pos + il + ll) as int, (pos + il + ll + contents.len()) as int) =~= contents + }), +{ + let id = encode_identifier(tag); + let ln = encode_length(contents.len()); + let il = id.len(); + let ll = ln.len(); + assert(encode_tlv(tag, contents) == id + ln + contents); + assert(id + ln + contents =~= id + (ln + contents)); + lemma_subrange_split(b, pos, id, ln + contents); + assert(pos + il + (ln + contents).len() <= end); + lemma_subrange_split(b, pos + il, ln, contents); + lemma_read_identifier_encode_identifier(tag, b, pos, end); + super::lemmas::lemma_pow256_8(); + lemma_read_length_encode_length(contents.len(), b, pos + il, end); +} + +/// The value that was read is the encoding of its tag and contents (DER +/// canonicity: the bytes are the one encoding the standard allows), and +/// its tag and length are ones the encoder accepts. +pub proof fn lemma_encode_tlv_read_tlv(b: Seq, pos: nat, end: nat) + requires + end <= b.len(), + ensures + match read_tlv(b, pos, end) { + Some(t) => { + &&& tlv_ok(t.tag, b.subrange(t.cstart as int, t.cend as int)) + &&& t.cend <= end + &&& b.subrange(pos as int, t.cend as int) =~= encode_tlv(t.tag, b.subrange(t.cstart as int, t.cend as int)) + }, + None => true, + }, +{ + super::lemmas::lemma_pow256_8(); + super::lemmas::lemma_read_tlv_bounds(b, pos, end); + if let Some(t) = read_tlv(b, pos, end) { + let (tag, id_len) = read_identifier(b, pos, end).unwrap(); + lemma_encode_identifier_read_identifier(b, pos, end); + super::lemmas::lemma_read_identifier_bounds(b, pos, end); + let (len, len_len) = read_length(b, pos + id_len, end).unwrap(); + lemma_encode_length_read_length(b, pos + id_len, end); + let c = b.subrange(t.cstart as int, t.cend as int); + assert(c.len() == len); + assert(b.subrange(pos as int, t.cend as int) =~= b.subrange(pos as int, (pos + id_len) as int) + + b.subrange((pos + id_len) as int, (pos + id_len + len_len) as int) + c); + } +} + +} // verus! diff --git a/crates/clazel-der/src/lemmas_typed.rs b/crates/clazel-der/src/lemmas_typed.rs new file mode 100644 index 0000000..35cbd74 --- /dev/null +++ b/crates/clazel-der/src/lemmas_typed.rs @@ -0,0 +1,655 @@ +//! Facts proven about the typed readers and encoders of the spec +//! ([`super::spec`]): for each universal type, reading what the encoder +//! wrote gives back the value (round trip), and encoding what the reader +//! read gives back the original bytes (DER canonicity, so no two byte strings +//! are accepted as the same value). Each canonicity lemma states its class +//! in its postcondition: the INTEGERs it covers are the non-negative ones. +//! Evidence that the transcription is right, not part of what the +//! implementation must meet. + +#[allow(unused_imports)] +use super::spec::*; +#[allow(unused_imports)] +use vstd::prelude::*; + +verus! { + +// --------------------------------------------------------------------------- +// BOOLEAN. +// --------------------------------------------------------------------------- + +/// A BOOLEAN's contents are the one octet the encoder writes. +pub proof fn lemma_boolean_contents(v: bool) + ensures + boolean_contents(seq![if v { 0xFFu8 } else { 0x00u8 }]) == Some(v), +{ +} + +/// Reading what [`encode_boolean`] wrote. +pub proof fn lemma_read_boolean_encode_boolean(v: bool, b: Seq, pos: nat, end: nat) + requires + pos + encode_boolean(v).len() <= end, + end <= b.len(), + b.subrange(pos as int, (pos + encode_boolean(v).len()) as int) =~= encode_boolean(v), + ensures + read_boolean(b, pos, end) == Some((v, (pos + encode_boolean(v).len()) as nat)), +{ + super::lemmas::lemma_pow256_8(); + let contents = seq![if v { 0xFFu8 } else { 0x00u8 }]; + let tag = universal_primitive(BOOLEAN); + super::lemmas_tlv::lemma_read_tlv_encode_tlv(tag, contents, b, pos, end); + lemma_boolean_contents(v); +} + +/// Writing what [`read_boolean`] read: the bytes are the encoding of the +/// value that was read. +pub proof fn lemma_encode_boolean_read_boolean(b: Seq, pos: nat, end: nat) + requires + end <= b.len(), + ensures + match read_boolean(b, pos, end) { + Some((v, next)) => next <= end && b.subrange(pos as int, next as int) =~= encode_boolean(v), + None => true, + }, +{ + super::lemmas_tlv::lemma_encode_tlv_read_tlv(b, pos, end); + if let Some((v, next)) = read_boolean(b, pos, end) { + let t = read_tlv(b, pos, end).unwrap(); + let c = b.subrange(t.cstart as int, t.cend as int); + assert(c =~= seq![if v { 0xFFu8 } else { 0x00u8 }]); + } +} + +// --------------------------------------------------------------------------- +// INTEGER. +// --------------------------------------------------------------------------- + +/// One octet spells itself. +pub proof fn lemma_be_value_single(x: u8) + ensures + be_value(seq![x]) == x as nat, +{ + assert(seq![x].drop_last() =~= Seq::::empty()); + assert(be_value(Seq::::empty()) == 0); +} + +/// Leading zeros do not change the number an octet string spells. +pub proof fn lemma_strip_leading_zeros_value(m: Seq) + ensures + be_value(strip_leading_zeros(m)) == be_value(m), + strip_leading_zeros(m).len() <= m.len(), + strip_leading_zeros(m).len() >= 1 ==> strip_leading_zeros(m)[0] != 0, + decreases m.len(), +{ + if m.len() > 0 && m[0] == 0 { + let d = m.skip(1); + lemma_strip_leading_zeros_value(d); + assert(d =~= m.drop_first()); + lemma_be_value_leading_zero_skip(m); + } +} + +/// An octet string with a zero first octet spells the same number as the +/// rest. +pub proof fn lemma_be_value_leading_zero_skip(m: Seq) + requires + m.len() >= 1, + m[0] == 0, + ensures + be_value(m) == be_value(m.skip(1)), + decreases m.len(), +{ + if m.len() > 1 { + let d = m.drop_last(); + assert(d[0] == 0); + lemma_be_value_leading_zero_skip(d); + assert(d.skip(1) =~= m.skip(1).drop_last()); + assert(m.skip(1).last() == m.last()); + assert(m.skip(1).len() >= 1); + } else { + assert(m.skip(1) =~= Seq::::empty()); + assert(m.drop_last() =~= Seq::::empty()); + } +} + +/// The contents the encoder writes for a non-negative number are a valid +/// INTEGER, non-negative, and their magnitude (the octets after a padding +/// zero, if any) spells the same number. +pub proof fn lemma_integer_unsigned_contents(m: Seq) + ensures + ({ + let c = integer_unsigned_contents(m); + &&& integer_contents_ok(c) + &&& c[0] < 128 + &&& c.len() <= m.len() + 1 + &&& integer_value(c) == be_value(m) as int + &&& (if c.len() >= 2 && c[0] == 0 { + be_value(c.skip(1)) == be_value(m) + } else { + be_value(c) == be_value(m) + }) + }), +{ + lemma_strip_leading_zeros_value(m); + let s = strip_leading_zeros(m); + let c = integer_unsigned_contents(m); + if s.len() == 0 { + assert(c =~= seq![0u8]); + assert(be_value(s) == 0) by { + assert(s =~= Seq::::empty()); + } + lemma_be_value_single(0); + assert(be_value(c) == 0); + assert(be_value(c) == be_value(m)); + } else if s[0] >= 128 { + assert(c =~= seq![0u8] + s); + assert(c.len() >= 2); + assert(c[1] == s[0]); + assert(c.skip(1) =~= s); + lemma_be_value_leading_zero_skip(c); + assert(be_value(c.skip(1)) == be_value(m)); + assert(be_value(c) == be_value(m)); + } else { + assert(c =~= s); + assert(be_value(c) == be_value(m)); + } + assert(c[0] < 128); + // The value is non-negative, so the two's complement number is the + // octets' number. + assert(integer_value(c) == be_value(c) as int); +} + +/// Reading what [`encode_integer_unsigned`] wrote: a non-negative INTEGER +/// whose magnitude octets (a single `0x00` for zero, otherwise with no +/// leading zero) spell the number that was encoded. +pub proof fn lemma_read_integer_encode_integer_unsigned(m: Seq, b: Seq, pos: nat, end: nat) + requires + integer_unsigned_contents(m).len() < pow256(8), + pos + encode_integer_unsigned(m).len() <= end, + end <= b.len(), + b.subrange(pos as int, (pos + encode_integer_unsigned(m).len()) as int) =~= encode_integer_unsigned(m), + ensures + match read_integer(b, pos, end) { + Some((negative, ms, me, next)) => { + &&& !negative + &&& ms <= me + &&& me == next + &&& next == pos + encode_integer_unsigned(m).len() + &&& be_value(b.subrange(ms as int, me as int)) == be_value(m) + &&& (me - ms >= 2 ==> b[ms as int] != 0) + }, + None => false, + }, +{ + super::lemmas::lemma_pow256_8(); + lemma_integer_unsigned_contents(m); + let c = integer_unsigned_contents(m); + let tag = universal_primitive(INTEGER); + super::lemmas_tlv::lemma_read_tlv_encode_tlv(tag, c, b, pos, end); + lemma_strip_leading_zeros_value(m); + let s = strip_leading_zeros(m); + let il = encode_identifier(tag).len(); + let ll = encode_length(c.len()).len(); + let cstart = pos + il + ll; + let cend = cstart + c.len(); + let bc = b.subrange(cstart as int, cend as int); + assert(bc =~= c); + if c.len() >= 2 && c[0] == 0 { + assert(b.subrange(cstart as int + 1, cend as int) =~= c.skip(1)); + assert(c.skip(1) =~= s); + assert(b[cstart as int + 1] == c[1]); + assert(c[1] == s[0]); + } else { + assert(b.subrange(cstart as int, cend as int) =~= c); + if c.len() >= 2 { + assert(b[cstart as int] == c[0]); + } + } +} + +/// Writing what [`read_integer`] read, for the non-negative INTEGERs: the +/// bytes are the encoding of the magnitude that was read, and the value (the +/// two's complement number of the contents) is the number the magnitude +/// spells. For a negative INTEGER the lemma states only that the value is +/// negative. +pub proof fn lemma_encode_integer_read_integer(b: Seq, pos: nat, end: nat) + requires + end <= b.len(), + ensures + match read_integer(b, pos, end) { + Some((negative, ms, me, next)) => { + &&& next <= end + &&& ms <= me + &&& me == next + &&& pos < ms + &&& (negative ==> { + let t = read_tlv(b, pos, end).unwrap(); + integer_value(b.subrange(t.cstart as int, t.cend as int)) < 0 + }) + &&& (!negative ==> { + let t = read_tlv(b, pos, end).unwrap(); + &&& integer_value(b.subrange(t.cstart as int, t.cend as int)) == be_value(b.subrange(ms as int, me as int)) as int + &&& b.subrange(pos as int, next as int) =~= encode_integer_unsigned(b.subrange(ms as int, me as int)) + }) + }, + None => true, + }, +{ + super::lemmas_tlv::lemma_encode_tlv_read_tlv(b, pos, end); + super::lemmas::lemma_read_tlv_bounds(b, pos, end); + if let Some((negative, ms, me, next)) = read_integer(b, pos, end) { + let t = read_tlv(b, pos, end).unwrap(); + let c = b.subrange(t.cstart as int, t.cend as int); + assert(c[0] == b[t.cstart as int]); + if negative { + super::lemmas::lemma_be_value_bound(c); + } else { + assert(integer_value(c) == be_value(c) as int); + let mag = b.subrange(ms as int, me as int); + if c.len() >= 2 && c[0] == 0 { + assert(c[1] >= 128); + assert(mag =~= c.skip(1)); + assert(mag[0] == c[1]); + lemma_be_value_leading_zero_skip(c); + assert(strip_leading_zeros(mag) =~= mag); + assert(integer_unsigned_contents(mag) =~= seq![0u8] + mag); + assert(seq![0u8] + mag =~= c); + } else { + assert(mag =~= c); + if c[0] == 0 { + // The single octet 0x00: the number zero. + assert(c.len() == 1); + assert(c =~= seq![0u8]); + assert(mag.drop_first() =~= Seq::::empty()); + assert(strip_leading_zeros(Seq::::empty()) =~= Seq::::empty()); + assert(strip_leading_zeros(mag) =~= Seq::::empty()); + assert(integer_unsigned_contents(mag) =~= seq![0u8]); + } else { + assert(strip_leading_zeros(mag) =~= mag); + assert(integer_unsigned_contents(mag) =~= mag); + } + } + assert(integer_unsigned_contents(mag) =~= c); + } + } +} + +// --------------------------------------------------------------------------- +// BIT STRING. +// --------------------------------------------------------------------------- + +/// Whether `unused` and `bits` make the contents of a DER BIT STRING. +pub open spec fn bit_string_ok(unused: u8, bits: Seq) -> bool { + bit_string_contents_ok(seq![unused] + bits) +} + +/// Reading what [`encode_bit_string`] wrote. +pub proof fn lemma_read_bit_string_encode_bit_string(unused: u8, bits: Seq, b: Seq, pos: nat, end: nat) + requires + bit_string_ok(unused, bits), + bits.len() + 1 < pow256(8), + pos + encode_bit_string(unused, bits).len() <= end, + end <= b.len(), + b.subrange(pos as int, (pos + encode_bit_string(unused, bits).len()) as int) =~= encode_bit_string(unused, bits), + ensures + match read_bit_string(b, pos, end) { + Some((u, ms, me, next)) => { + &&& u == unused + &&& next == pos + encode_bit_string(unused, bits).len() + &&& b.subrange(ms as int, me as int) =~= bits + }, + None => false, + }, +{ + let c = seq![unused] + bits; + let tag = universal_primitive(BIT_STRING); + assert(tlv_ok(tag, c)); + super::lemmas_tlv::lemma_read_tlv_encode_tlv(tag, c, b, pos, end); + let il = encode_identifier(tag).len(); + let ll = encode_length(c.len()).len(); + let cstart = pos + il + ll; + let bc = b.subrange(cstart as int, (cstart + c.len()) as int); + assert(bc =~= c); + assert(b.subrange(cstart as int + 1, (cstart + c.len()) as int) =~= bits) by { + assert forall|i: int| 0 <= i < bits.len() implies #[trigger] b.subrange(cstart as int + 1, (cstart + c.len()) as int)[i] + == bits[i] by { + assert(bc[1 + i] == c[1 + i]); + assert(c[1 + i] == bits[i]); + } + } + assert(c[0] == unused); + if c.len() >= 2 { + assert(bc.last() == c.last()); + } +} + +/// Writing what [`read_bit_string`] read: the bits' octets and unused count +/// make a valid BIT STRING, and the bytes are its encoding. +pub proof fn lemma_encode_bit_string_read_bit_string(b: Seq, pos: nat, end: nat) + requires + end <= b.len(), + ensures + match read_bit_string(b, pos, end) { + Some((u, ms, me, next)) => { + &&& bit_string_ok(u, b.subrange(ms as int, me as int)) + &&& next <= end + &&& b.subrange(pos as int, next as int) =~= encode_bit_string(u, b.subrange(ms as int, me as int)) + }, + None => true, + }, +{ + super::lemmas_tlv::lemma_encode_tlv_read_tlv(b, pos, end); + if let Some((u, ms, me, next)) = read_bit_string(b, pos, end) { + let t = read_tlv(b, pos, end).unwrap(); + let c = b.subrange(t.cstart as int, t.cend as int); + assert(c[0] == u); + assert(b.subrange(ms as int, me as int) =~= c.skip(1)); + assert(c =~= seq![u] + b.subrange(ms as int, me as int)); + } +} + +// --------------------------------------------------------------------------- +// OCTET STRING and the character string types. +// --------------------------------------------------------------------------- + +/// Reading what [`encode_octets`] wrote (an OCTET STRING, UTF8String, +/// PrintableString, IA5String, UTCTime or GeneralizedTime). +pub proof fn lemma_read_octets_encode_octets(number: u32, s: Seq, b: Seq, pos: nat, end: nat) + requires + number <= MAX_TAG_NUMBER, + s.len() < pow256(8), + pos + encode_octets(number, s).len() <= end, + end <= b.len(), + b.subrange(pos as int, (pos + encode_octets(number, s).len()) as int) =~= encode_octets(number, s), + ensures + match read_octets(b, pos, end, number) { + Some((cs, ce)) => ce == pos + encode_octets(number, s).len() && b.subrange(cs as int, ce as int) =~= s, + None => false, + }, +{ + super::lemmas_tlv::lemma_read_tlv_encode_tlv(universal_primitive(number), s, b, pos, end); +} + +/// Writing what [`read_octets`] read. +pub proof fn lemma_encode_octets_read_octets(b: Seq, pos: nat, end: nat, number: u32) + requires + end <= b.len(), + ensures + match read_octets(b, pos, end, number) { + Some((cs, ce)) => { + &&& ce <= end + &&& b.subrange(pos as int, ce as int) =~= encode_octets(number, b.subrange(cs as int, ce as int)) + }, + None => true, + }, +{ + super::lemmas_tlv::lemma_encode_tlv_read_tlv(b, pos, end); +} + +/// A constructed string is rejected (§10.2): where a value's tag has bit 6 +/// set, no string reader returns it. +pub proof fn lemma_read_octets_rejects_constructed(b: Seq, pos: nat, end: nat, number: u32) + requires + read_tlv(b, pos, end) is Some, + read_tlv(b, pos, end).unwrap().tag.constructed, + ensures + read_octets(b, pos, end, number) is None, +{ +} + +// --------------------------------------------------------------------------- +// NULL. +// --------------------------------------------------------------------------- + +/// Reading what [`encode_null`] wrote. +pub proof fn lemma_read_null_encode_null(b: Seq, pos: nat, end: nat) + requires + pos + 2 <= end, + end <= b.len(), + b.subrange(pos as int, pos as int + 2) =~= encode_null(), + ensures + read_null(b, pos, end) == Some((pos + 2) as nat), +{ + super::lemmas::lemma_pow256_8(); + assert(encode_null() =~= seq![5u8, 0u8]) by { + assert(encode_identifier(universal_primitive(NULL)) =~= seq![5u8]); + assert(encode_length(0) =~= seq![0u8]); + } + super::lemmas_tlv::lemma_read_tlv_encode_tlv(universal_primitive(NULL), seq![], b, pos, end); +} + +/// Writing what [`read_null`] read: `05 00`, and nothing else. +pub proof fn lemma_encode_null_read_null(b: Seq, pos: nat, end: nat) + requires + end <= b.len(), + ensures + match read_null(b, pos, end) { + Some(next) => next == pos + 2 && next <= end && b.subrange(pos as int, next as int) =~= encode_null(), + None => true, + }, +{ + super::lemmas_tlv::lemma_encode_tlv_read_tlv(b, pos, end); + super::lemmas::lemma_read_tlv_bounds(b, pos, end); + if let Some(next) = read_null(b, pos, end) { + let t = read_tlv(b, pos, end).unwrap(); + assert(b.subrange(t.cstart as int, t.cend as int) =~= Seq::::empty()); + assert(encode_null() =~= seq![5u8, 0u8]) by { + assert(encode_identifier(universal_primitive(NULL)) =~= seq![5u8]); + assert(encode_length(0) =~= seq![0u8]); + } + } +} + +// --------------------------------------------------------------------------- +// OBJECT IDENTIFIER. +// --------------------------------------------------------------------------- + +/// Reading what [`encode_oid`] wrote: every valid list of components +/// survives. +pub proof fn lemma_read_oid_encode_oid(arcs: Seq, b: Seq, pos: nat, end: nat) + requires + oid_valid(arcs), + oid_contents(arcs).len() < pow256(8), + pos + encode_oid(arcs).len() <= end, + end <= b.len(), + b.subrange(pos as int, (pos + encode_oid(arcs).len()) as int) =~= encode_oid(arcs), + ensures + read_oid(b, pos, end) == Some((arcs, (pos + encode_oid(arcs).len()) as nat)), +{ + let tag = universal_primitive(OBJECT_IDENTIFIER); + super::lemmas_tlv::lemma_read_tlv_encode_tlv(tag, oid_contents(arcs), b, pos, end); + super::lemmas_oid::lemma_oid_components_oid_contents(arcs); +} + +/// Writing what [`read_oid`] read: the components are valid, and the bytes +/// are their encoding. There are no more components than contents octets +/// plus one. +pub proof fn lemma_encode_oid_read_oid(b: Seq, pos: nat, end: nat) + requires + end <= b.len(), + ensures + match read_oid(b, pos, end) { + Some((arcs, next)) => { + &&& oid_valid(arcs) + &&& next <= end + &&& b.subrange(pos as int, next as int) =~= encode_oid(arcs) + &&& arcs.len() <= next - pos + }, + None => true, + }, +{ + super::lemmas_tlv::lemma_encode_tlv_read_tlv(b, pos, end); + super::lemmas::lemma_read_tlv_bounds(b, pos, end); + if let Some((arcs, next)) = read_oid(b, pos, end) { + let t = read_tlv(b, pos, end).unwrap(); + let c = b.subrange(t.cstart as int, t.cend as int); + super::lemmas_oid::lemma_oid_contents_oid_components(c); + assert(c.len() + 1 <= next - pos); + } +} + +// --------------------------------------------------------------------------- +// SEQUENCE, SET, and context-specific tags. +// --------------------------------------------------------------------------- + +/// Reading what [`encode_sequence`] wrote: the contents range holds exactly +/// the contents. +pub proof fn lemma_expect_sequence_encode_sequence(contents: Seq, b: Seq, pos: nat, end: nat) + requires + contents.len() < pow256(8), + pos + encode_sequence(contents).len() <= end, + end <= b.len(), + b.subrange(pos as int, (pos + encode_sequence(contents).len()) as int) =~= encode_sequence(contents), + ensures + match expect_sequence(b, pos, end) { + Some((cs, ce)) => ce == pos + encode_sequence(contents).len() && b.subrange(cs as int, ce as int) =~= contents, + None => false, + }, +{ + super::lemmas_tlv::lemma_read_tlv_encode_tlv(universal_constructed(SEQUENCE), contents, b, pos, end); +} + +/// Writing what [`expect_sequence`] read. +pub proof fn lemma_encode_sequence_expect_sequence(b: Seq, pos: nat, end: nat) + requires + end <= b.len(), + ensures + match expect_sequence(b, pos, end) { + Some((cs, ce)) => ce <= end && b.subrange(pos as int, ce as int) =~= encode_sequence(b.subrange(cs as int, ce as int)), + None => true, + }, +{ + super::lemmas_tlv::lemma_encode_tlv_read_tlv(b, pos, end); +} + +/// Reading what [`encode_set`] wrote. +pub proof fn lemma_expect_set_encode_set(contents: Seq, b: Seq, pos: nat, end: nat) + requires + contents.len() < pow256(8), + pos + encode_set(contents).len() <= end, + end <= b.len(), + b.subrange(pos as int, (pos + encode_set(contents).len()) as int) =~= encode_set(contents), + ensures + match expect_set(b, pos, end) { + Some((cs, ce)) => ce == pos + encode_set(contents).len() && b.subrange(cs as int, ce as int) =~= contents, + None => false, + }, +{ + super::lemmas_tlv::lemma_read_tlv_encode_tlv(universal_constructed(SET), contents, b, pos, end); +} + +/// Writing what [`expect_set`] read. +pub proof fn lemma_encode_set_expect_set(b: Seq, pos: nat, end: nat) + requires + end <= b.len(), + ensures + match expect_set(b, pos, end) { + Some((cs, ce)) => ce <= end && b.subrange(pos as int, ce as int) =~= encode_set(b.subrange(cs as int, ce as int)), + None => true, + }, +{ + super::lemmas_tlv::lemma_encode_tlv_read_tlv(b, pos, end); +} + +/// Reading what [`encode_context`] wrote: the component is present, with +/// the form and contents that were written (§8.14.3 and §8.14.4). +pub proof fn lemma_read_optional_context_encode_context(number: u32, constructed: bool, contents: Seq, b: Seq, pos: nat, end: nat) + requires + number <= MAX_TAG_NUMBER, + contents.len() < pow256(8), + pos + encode_context(number, constructed, contents).len() <= end, + end <= b.len(), + b.subrange(pos as int, (pos + encode_context(number, constructed, contents).len()) as int) =~= encode_context( + number, + constructed, + contents, + ), + ensures + match read_optional_context(b, pos, end, number) { + Some(Some((c, cs, ce))) => { + &&& c == constructed + &&& ce == pos + encode_context(number, constructed, contents).len() + &&& b.subrange(cs as int, ce as int) =~= contents + }, + _ => false, + }, +{ + let tag = context_specific(number, constructed); + super::lemmas_tlv::lemma_read_tlv_encode_tlv(tag, contents, b, pos, end); + super::lemmas_tlv::lemma_read_identifier_encode_identifier(tag, b, pos, end); +} + +/// Reading what [`encode_context`] wrote as an EXPLICIT tag (§8.14.3, always +/// constructed): the inner range holds the one complete encoding. +pub proof fn lemma_read_optional_explicit_encode_context(number: u32, contents: Seq, b: Seq, pos: nat, end: nat) + requires + number <= MAX_TAG_NUMBER, + contents.len() < pow256(8), + pos + encode_context(number, true, contents).len() <= end, + end <= b.len(), + b.subrange(pos as int, (pos + encode_context(number, true, contents).len()) as int) =~= encode_context(number, true, contents), + ensures + match read_optional_explicit(b, pos, end, number) { + Some(Some((cs, ce))) => { + &&& ce == pos + encode_context(number, true, contents).len() + &&& b.subrange(cs as int, ce as int) =~= contents + }, + _ => false, + }, +{ + lemma_read_optional_context_encode_context(number, true, contents, b, pos, end); +} + +/// Writing what [`read_optional_context`] read, when it found the +/// component. +pub proof fn lemma_encode_context_read_optional_context(b: Seq, pos: nat, end: nat, number: u32) + requires + end <= b.len(), + ensures + match read_optional_context(b, pos, end, number) { + Some(Some((c, cs, ce))) => { + &&& number <= MAX_TAG_NUMBER + &&& ce <= end + &&& b.subrange(pos as int, ce as int) =~= encode_context(number, c, b.subrange(cs as int, ce as int)) + }, + _ => true, + }, +{ + super::lemmas_tlv::lemma_encode_tlv_read_tlv(b, pos, end); + super::lemmas::lemma_read_tlv_bounds(b, pos, end); + super::lemmas::lemma_read_identifier_bounds(b, pos, end); +} + +/// A component is absent when nothing is left, and when the next value has +/// another tag. +pub proof fn lemma_read_optional_context_absent(b: Seq, pos: nat, end: nat, number: u32) + ensures + pos >= end ==> read_optional_context(b, pos, end, number) == Some(None::<(bool, nat, nat)>), + (match read_identifier(b, pos, end) { + Some((tag, _)) => !(tag.class == Class::ContextSpecific && tag.number == number), + None => false, + }) ==> read_optional_context(b, pos, end, number) == Some(None::<(bool, nat, nat)>), +{ +} + + +/// Writing what [`read_optional_explicit`] read, when it found the +/// component: the bytes are the encoding of a constructed `[number]` around +/// the inner range (§8.14.3). +pub proof fn lemma_encode_context_read_optional_explicit(b: Seq, pos: nat, end: nat, number: u32) + requires + end <= b.len(), + ensures + match read_optional_explicit(b, pos, end, number) { + Some(Some((cs, ce))) => { + &&& number <= MAX_TAG_NUMBER + &&& ce <= end + &&& b.subrange(pos as int, ce as int) =~= encode_context(number, true, b.subrange(cs as int, ce as int)) + }, + _ => true, + }, +{ + lemma_encode_context_read_optional_context(b, pos, end, number); +} + +} // verus! diff --git a/crates/clazel-der/src/lib.rs b/crates/clazel-der/src/lib.rs new file mode 100644 index 0000000..08d5c10 --- /dev/null +++ b/crates/clazel-der/src/lib.rs @@ -0,0 +1,30 @@ +//! ASN.1 Distinguished Encoding Rules: the decoder and encoder that X.509 +//! certificates (item 3.5b) and PKCS#1 signatures (item 3.5c) are built on. +//! +//! Spec: ITU-T X.690 (08/2015), "Information technology - ASN.1 encoding +//! rules: Specification of Basic Encoding Rules (BER), Canonical Encoding +//! Rules (CER) and Distinguished Encoding Rules (DER)", clause 8 as +//! restricted by clauses 10 and 11 +//! (). +//! Tier: see `[package.metadata.clazel]` in `Cargo.toml` and +//! `docs/VERIFICATION.md`. +//! +//! - [`spec`]: a step-by-step transcription of the standard. +//! - [`contract`]: what the executable functions are proven to satisfy. +//! +//! Strings are `Seq` in the spec and `&[u8]` in the executable code. +//! The decoder is a cursor: every reader takes the input, a position and an +//! end, reads one value at the position and returns where it ended. + +pub mod contract; +pub mod decode; +pub mod encode; +pub mod examples; +pub mod independent; +pub mod lemmas; +pub mod lemmas_oid; +pub mod lemmas_overread; +pub mod lemmas_tlv; +pub mod lemmas_typed; +pub mod spec; +pub mod termination; diff --git a/crates/clazel-der/src/spec.rs b/crates/clazel-der/src/spec.rs new file mode 100644 index 0000000..6382840 --- /dev/null +++ b/crates/clazel-der/src/spec.rs @@ -0,0 +1,1069 @@ +//! Executable-style specification of ASN.1 DER, transcribed from ITU-T X.690 +//! (08/2015), "Information technology - ASN.1 encoding rules: Specification +//! of Basic Encoding Rules (BER), Canonical Encoding Rules (CER) and +//! Distinguished Encoding Rules (DER)" (also ISO/IEC 8825-1:2015, +//! ): clause 8 (the basic +//! encoding rules) with clause 10 (the DER restrictions on length forms and +//! string encodings) and clause 11 (the restrictions both CER and DER +//! apply: BOOLEAN, unused bits). An ITU-T Recommendation is a dated edition, +//! so there is no commit to pin; the tag numbers of the universal types are +//! those of ITU-T X.680 (08/2015) Table 1, which X.690 itself uses without +//! listing. Nothing here runs; it defines what "correct" means. Each +//! `// N.` comment is a clause of the standard cited next to it. +//! +//! ## The profile +//! +//! The decoder accepts exactly the DER encodings, never the other BER +//! alternatives (X.690 §7.4: "No alternative encodings are permitted by the +//! Canonical Encoding Rules or Distinguished Encoding Rules"): +//! - definite length only (§10.1; the indefinite form of §8.1.3.6 is +//! rejected) and in the minimum number of octets (§10.1), so a length that +//! fits the short form never uses the long form and the long form never +//! has a leading zero octet; +//! - an identifier in the low-tag-number form for tag numbers 0 to 30 +//! (§8.1.2.2) and the high-tag-number form only from 31 (§8.1.2.4), with no +//! leading zero digit (§8.1.2.4.2 c); +//! - BOOLEAN as `0x00` or `0xFF` (§8.2, §11.1), INTEGER with no redundant +//! leading octets (§8.3.2), BIT STRING in the primitive form with an +//! unused-bits octet of 0 to 7 (§8.6.2.2), zero where the string is empty +//! (§8.6.2.3), and zero unused bits (§11.2.1), OCTET STRING and the +//! character string types in the primitive form (§10.2: "the constructed +//! form of encoding shall not be used"), NULL with no contents (§8.8), +//! OBJECT IDENTIFIER with no padding digit (§8.19.2), SEQUENCE and SET in +//! the constructed form (§8.9.1, §8.11.1), and a tagged type either +//! implicit (§8.14.4) or explicit and constructed (§8.14.3). +//! +//! What this file leaves to its callers, as the roadmap states (item 3.5a): +//! the character and date restrictions of UTF8String, PrintableString, +//! IA5String, UTCTime and GeneralizedTime (RFC 5280 §4.1.2.5, X.509's job), +//! the order of SET OF components (§11.6, checked by X.509 where it +//! matters), and omission of DEFAULT values (§11.5, the schema's job). +//! +//! ## Modelling choices +//! +//! Each is a deviation in form, never in meaning, except where marked as a +//! limit, and the argument is given here once: +//! - **A reader is a cursor.** The standard defines the encoding of a data +//! value; a decoder is a function from bytes to the value and its extent. +//! Every reader takes the input `b`, a position `pos` and an `end`, reads +//! the one value at `pos`, and returns where it stopped. Nothing at or +//! after `end` exists to it: `end` stands for the end of the enclosing +//! value's contents, so "descending into a constructed value" is calling +//! the readers again on `[cstart, cend)`, and "end of contents" is +//! [`expect_end`]. +//! - **"Failure" is `None`.** A byte string that is not the DER encoding of a +//! value of the requested type gives `None`; the standard has no failure +//! value, only conformance (§7). +//! - **The next position is the end of the contents.** With the definite +//! length (§8.1.3.3) there are no end-of-contents octets (§8.1.5), so a +//! value ends exactly where its contents end: a [`Tlv`] has no field for +//! it, and `cend` is it. +//! - **"Bits 7 to 1" and "bit 8" are arithmetic.** Bit 8 of an octet `o` is +//! `o / 128`, bits 7 to 1 are `o % 128`; the class (bits 8 and 7) is +//! `o / 64` and bit 6 is `(o / 32) % 2` (§8.1.2.2, Table 1). +//! - **Numbers "encoded as an unsigned binary integer" are `nat`s.** A +//! run of octets spelling a number base 256 is [`be_value`] and a run of +//! subsequent octets of a tag or subidentifier is [`base128`] (bit 8 of +//! each octet is the continuation flag and is not part of the number). +//! - **An INTEGER is read as its sign and a range.** §8.3.3 makes the +//! contents a two's complement number; [`integer_value`] is that number, +//! and a reader gives its sign and the magnitude octets without the single +//! `0x00` octet that pads a positive number whose first octet has bit 8 +//! set. The magnitude of a negative number is its two's complement octets +//! unchanged: they are not negated. +//! - **A BIT STRING is read as its unused-bits count and its octets.** +//! - **An OBJECT IDENTIFIER is read as its components** (`Seq`), by +//! §8.19.3 to §8.19.5. +//! - **Limits, which are choices of this project.** Each only rejects +//! encodings the standard accepts, and none can matter on an input shorter +//! than 2^64 bytes except the first and the third: +//! - a tag number of 2^14 or more is rejected (the standard has no bound; +//! X.509 uses tag numbers 0 to 30, and the high form is parsed up to +//! 16383 so it can be recognised); +//! - a long-form length of more than 8 octets is rejected, which is what +//! would reject a length of 2^64 or more; a content that long is not +//! inside any input of fewer than 2^64 bytes, so the effect is nil +//! there; +//! - a subidentifier of 2^64 or more is rejected (no object identifier +//! in use has one, and an arc is a `u64` in the executable code). +//! - **Tag numbers are `u32`.** The only tag numbers that survive the limit +//! above are below 2^14. +//! - **Constructed strings.** The standard lets BER senders encode a BIT +//! STRING or OCTET STRING in the constructed form (§8.6.3, §8.7.3) and DER +//! forbids it (§10.2). No reader accepts a constructed form of a string +//! type: the readers require the primitive form of the universal tag, so +//! the tag with bit 6 set is "another tag" and is rejected. +//! +//! Where the standard is silent but a DER reader must decide, the choice is +//! listed here: a high-tag-number identifier with a number below 31 is +//! rejected (§8.1.2.2 requires the low form for 0 to 30, so the other +//! encoding is not the one DER allows, §7.4); the length octets +//! `0xFF` (reserved, §8.1.3.5 c) and `0x80` (the indefinite form, §8.1.3.6) +//! are rejected; an OBJECT IDENTIFIER has at least one subidentifier +//! (§8.19.3: N is one less than the number of components, and X.680 §32.3 +//! gives an object identifier at least two components). + +#[allow(unused_imports)] +use vstd::prelude::*; + +verus! { + +// --------------------------------------------------------------------------- +// Data model. +// --------------------------------------------------------------------------- + +/// The class of a tag (§8.1.2.2, Table 1: "Encoding of class of tag"). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Class { + /// Bits 8 and 7 are `00`. + Universal, + /// Bits 8 and 7 are `01`. + Application, + /// Bits 8 and 7 are `10`. + ContextSpecific, + /// Bits 8 and 7 are `11`. + Private, +} + +/// An ASN.1 tag as the identifier octets carry it (§8.1.2.1: "the identifier +/// octets shall encode the ASN.1 tag (class and number) of the type of the +/// data value"), with the primitive/constructed bit (§8.1.2.5). The number +/// is below 2^14 in everything this crate produces (see the limits above). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Tag { + /// Bits 8 and 7 of the first identifier octet. + pub class: Class, + /// Bit 6 of the first identifier octet: 1 if the encoding is + /// constructed (§8.1.2.5). + pub constructed: bool, + /// The tag number. + pub number: u32, +} + +/// A data value's extent: its tag and its contents range `[cstart, cend)`. +/// The value ends at `cend` (no end-of-contents octets, §8.1.5). +pub struct Tlv { + /// The tag from the identifier octets. + pub tag: Tag, + /// Index of the first contents octet. + pub cstart: nat, + /// Index after the last contents octet. + pub cend: nat, +} + +/// The largest tag number the decoder accepts (a limit of this project). +pub const MAX_TAG_NUMBER: u32 = 16383; + +/// The most octets after the initial one in a long-form length (a limit of +/// this project). +pub const MAX_LENGTH_OCTETS: u32 = 8; + +/// Tag numbers of the universal types (ITU-T X.680 Table 1). +pub const BOOLEAN: u32 = 1; + +/// INTEGER. +pub const INTEGER: u32 = 2; + +/// BIT STRING. +pub const BIT_STRING: u32 = 3; + +/// OCTET STRING. +pub const OCTET_STRING: u32 = 4; + +/// NULL. +pub const NULL: u32 = 5; + +/// OBJECT IDENTIFIER. +pub const OBJECT_IDENTIFIER: u32 = 6; + +/// UTF8String. +pub const UTF8_STRING: u32 = 12; + +/// SEQUENCE and SEQUENCE OF. +pub const SEQUENCE: u32 = 16; + +/// SET and SET OF. +pub const SET: u32 = 17; + +/// PrintableString. +pub const PRINTABLE_STRING: u32 = 19; + +/// IA5String. +pub const IA5_STRING: u32 = 22; + +/// UTCTime. +pub const UTC_TIME: u32 = 23; + +/// GeneralizedTime. +pub const GENERALIZED_TIME: u32 = 24; + +/// The tag of a universal type with a primitive encoding. +pub open spec fn universal_primitive(number: u32) -> Tag { + Tag { class: Class::Universal, constructed: false, number } +} + +/// The tag of a universal type with a constructed encoding. +pub open spec fn universal_constructed(number: u32) -> Tag { + Tag { class: Class::Universal, constructed: true, number } +} + +/// The tag of a context-specific type (`[number]`). +pub open spec fn context_specific(number: u32, constructed: bool) -> Tag { + Tag { class: Class::ContextSpecific, constructed, number } +} + +// --------------------------------------------------------------------------- +// Numbers. +// --------------------------------------------------------------------------- + +/// 256 to the power `n`: the number of values `n` octets can hold. +pub open spec fn pow256(n: nat) -> nat + decreases n, +{ + if n == 0 { + 1 + } else { + 256 * pow256((n - 1) as nat) + } +} + +/// "An unsigned binary integer ... with bit 8 of the first subsequent octet +/// as the most significant bit" (§8.1.3.5): the number `s` spells base 256, +/// most significant octet first. +pub open spec fn be_value(s: Seq) -> nat + decreases s.len(), +{ + if s.len() == 0 { + 0 + } else { + be_value(s.drop_last()) * 256 + s.last() as nat + } +} + +/// The `width` octets spelling `value` base 256, most significant first +/// (the inverse of [`be_value`] for `value < 256^width`). +pub open spec fn be_bytes(value: nat, width: nat) -> Seq + decreases width, +{ + if width == 0 { + seq![] + } else { + be_bytes(value / 256, (width - 1) as nat).push((value % 256) as u8) + } +} + +/// "Bits 7 to 1 of the first subsequent octet, followed by bits 7 to 1 of +/// the second ... shall be the encoding of an unsigned binary integer, with +/// bit 7 of the first subsequent octet as the most significant bit" +/// (§8.1.2.4.2 b), and the same for a subidentifier (§8.19.2): the number +/// the low seven bits of each octet of `s` spell, most significant first. +pub open spec fn base128(s: Seq) -> nat + decreases s.len(), +{ + if s.len() == 0 { + 0 + } else { + base128(s.drop_last()) * 128 + (s.last() % 128) as nat + } +} + +/// "Bit 8 of each octet shall be set to one unless it is the last octet" +/// (§8.1.2.4.2 a), §8.19.2): the index of the first octet of `b[i..end)` +/// whose bit 8 is zero, which ends the series. +pub open spec fn series_end(b: Seq, i: nat, end: nat) -> Option + decreases end - i, +{ + if i >= end { + None + } else if b[i as int] < 128 { + Some(i) + } else { + series_end(b, i + 1, end) + } +} + +// --------------------------------------------------------------------------- +// §8.1.2: identifier octets. +// --------------------------------------------------------------------------- + +/// Table 1: the class encoded by bits 8 and 7 (as the number `o / 64`). +pub open spec fn class_from_bits(bits: u8) -> Class { + if bits == 0 { + Class::Universal + } else if bits == 1 { + Class::Application + } else if bits == 2 { + Class::ContextSpecific + } else { + Class::Private + } +} + +/// Table 1, the other way: the value of bits 8 and 7. +pub open spec fn class_bits(class: Class) -> u8 { + match class { + Class::Universal => 0, + Class::Application => 1, + Class::ContextSpecific => 2, + Class::Private => 3, + } +} + +/// §8.1.2: read the identifier octets at `pos`. Returns the tag and the +/// number of identifier octets. +pub open spec fn read_identifier(b: Seq, pos: nat, end: nat) -> Option<(Tag, nat)> { + if pos >= end { + // The leading octet is missing. + None + } else { + let o = b[pos as int]; + // 8.1.2.2 a), 8.1.2.4.1 a): "bits 8 and 7 shall be encoded to + // represent the class of the tag as specified in Table 1". + let class = class_from_bits(o / 64); + // 8.1.2.2 b), 8.1.2.4.1 b), 8.1.2.5: "Bit 6 shall be set to zero if + // the encoding is primitive, and shall be set to one if the + // encoding is constructed." + let constructed = (o / 32) % 2 == 1; + if o % 32 != 31 { + // 1. 8.1.2.2 c): "bits 5 to 1 shall encode the number of the + // tag as a binary integer" (tag numbers 0 to 30). + Some((Tag { class, constructed, number: (o % 32) as u32 }, 1)) + } else { + // 2. 8.1.2.4: "For tags with a number greater than or equal to + // 31, the identifier shall comprise a leading octet followed + // by one or more subsequent octets." 8.1.2.4.1 c): the + // leading octet's bits 5 to 1 are 11111. + match series_end(b, pos + 1, end) { + // 8.1.2.4.2 a): the last subsequent octet is missing. + None => None, + Some(j) => { + // 8.1.2.4.2 b): the tag number. + let number = base128(b.subrange(pos as int + 1, j as int + 1)); + if b[pos as int + 1] % 128 == 0 { + // 8.1.2.4.2 c): "bits 7 to 1 of the first + // subsequent octet shall not all be zero". + None + } else if number < 31 { + // 8.1.2.2: tag numbers 0 to 30 are encoded in the + // low-tag-number form. (DER allows no alternative, + // §7.4.) + None + } else if number > MAX_TAG_NUMBER as nat { + // A limit of this project (see the header). + None + } else { + Some((Tag { class, constructed, number: number as u32 }, (j + 1 - pos) as nat)) + } + }, + } + } + } +} + +// --------------------------------------------------------------------------- +// §8.1.3: length octets, with §10.1. +// --------------------------------------------------------------------------- + +/// §8.1.3: read the length octets at `pos`. Returns the length (the number +/// of contents octets) and the number of length octets. +pub open spec fn read_length(b: Seq, pos: nat, end: nat) -> Option<(nat, nat)> { + if pos >= end { + // The initial length octet is missing. + None + } else { + let f = b[pos as int]; + if f < 128 { + // 1. 8.1.3.4: "In the short form, the length octets shall + // consist of a single octet in which bit 8 is zero and bits + // 7 to 1 encode the number of octets in the contents octets." + Some((f as nat, 1)) + } else if f == 128 { + // 2. 8.1.3.6: the indefinite form. 10.1: "The definite form of + // length encoding shall be used". + None + } else if f == 255 { + // 3. 8.1.3.5 c): "the value 11111111 shall not be used". + None + } else { + // 4. 8.1.3.5: the long form. b): "bits 7 to 1 shall encode the + // number of subsequent octets in the length octets". + let n = (f - 128) as nat; + if n > MAX_LENGTH_OCTETS as nat { + // A limit of this project (see the header). + None + } else if pos + 1 + n > end { + // The subsequent octets are missing. + None + } else if b[pos as int + 1] == 0 { + // 10.1: "encoded in the minimum number of octets": a + // leading zero octet is redundant. + None + } else { + // 8.1.3.5: "the encoding of an unsigned binary integer + // equal to the number of octets in the contents octets". + let v = be_value(b.subrange(pos as int + 1, pos as int + 1 + n as int)); + if v < 128 { + // 8.1.3.3 NOTE: the short form can be used for 127 or + // less, so 10.1 requires it. + None + } else { + Some((v, 1 + n)) + } + } + } + } +} + +// --------------------------------------------------------------------------- +// §8.1.1: an encoding is identifier, length and contents. +// --------------------------------------------------------------------------- + +/// §8.1.1.1: read the identifier octets, the length octets and check that +/// the contents octets lie inside `[pos, end)`. The end-of-contents octets +/// are not present (§8.1.1.2, §8.1.5: the length is definite). +pub open spec fn read_tlv(b: Seq, pos: nat, end: nat) -> Option { + match read_identifier(b, pos, end) { + None => None, + Some((tag, id_len)) => match read_length(b, pos + id_len, end) { + None => None, + Some((len, len_len)) => { + let cstart = pos + id_len + len_len; + if cstart + len > end { + // 8.1.1.1 c): the contents octets are missing. + None + } else { + Some(Tlv { tag, cstart, cend: cstart + len }) + } + }, + }, + } +} + +/// The end of contents: nothing is left in `[pos, end)`. A constructed +/// value's contents are "the complete encoding of one data value from each +/// of the types listed" (§8.9.2) and nothing more. +pub open spec fn expect_end(pos: nat, end: nat) -> bool { + pos == end +} + +// --------------------------------------------------------------------------- +// §8.2, §11.1: BOOLEAN. +// --------------------------------------------------------------------------- + +/// The value of a BOOLEAN's contents octets. +pub open spec fn boolean_contents(c: Seq) -> Option { + if c.len() != 1 { + // 8.2.1: "The contents octets shall consist of a single octet." + None + } else if c[0] == 0 { + // 8.2.2: "If the boolean value is FALSE the octet shall be zero." + Some(false) + } else if c[0] == 255 { + // 11.1: "If the encoding represents the boolean value TRUE, its + // single contents octet shall have all eight bits set to one." + Some(true) + } else { + None + } +} + +/// Read a BOOLEAN at `pos`. Returns its value and the next position. +pub open spec fn read_boolean(b: Seq, pos: nat, end: nat) -> Option<(bool, nat)> { + match read_tlv(b, pos, end) { + None => None, + Some(t) => { + // 8.2.1: "The encoding of a boolean value shall be primitive." + if t.tag == universal_primitive(BOOLEAN) { + match boolean_contents(b.subrange(t.cstart as int, t.cend as int)) { + Some(v) => Some((v, t.cend)), + None => None, + } + } else { + None + } + }, + } +} + +// --------------------------------------------------------------------------- +// §8.3: INTEGER. +// --------------------------------------------------------------------------- + +/// §8.3.2: "If the contents octets of an integer value encoding consist of +/// more than one octet, then the bits of the first octet and bit 8 of the +/// second octet: a) shall not all be ones; and b) shall not all be zero." +/// §8.3.1: "The contents octets shall consist of one or more octets." +pub open spec fn integer_contents_ok(c: Seq) -> bool { + &&& c.len() >= 1 + &&& c.len() >= 2 ==> !((c[0] == 255 && c[1] >= 128) || (c[0] == 0 && c[1] < 128)) +} + +/// §8.3.3: "The contents octets shall be a two's complement binary number +/// equal to the integer value": the value of `c` with its bit 8 of the first +/// octet weighing minus 2^(8n-1). +pub open spec fn integer_value(c: Seq) -> int { + if c.len() == 0 { + 0 + } else if c[0] >= 128 { + be_value(c) as int - pow256(c.len()) as int + } else { + be_value(c) as int + } +} + +/// Read an INTEGER at `pos`. Returns whether it is negative, the range of +/// its magnitude octets (see the modelling choices) and the next position. +pub open spec fn read_integer(b: Seq, pos: nat, end: nat) -> Option<(bool, nat, nat, nat)> { + match read_tlv(b, pos, end) { + None => None, + Some(t) => { + let c = b.subrange(t.cstart as int, t.cend as int); + // 8.3.1: primitive. + if t.tag == universal_primitive(INTEGER) && integer_contents_ok(c) { + // 8.3.3: the value is negative if bit 8 of the first octet + // is one. + let negative = c[0] >= 128; + // A leading 0x00 octet only pads a positive number (8.3.2 + // b) makes it needed exactly when the next octet has bit 8 + // set). + let start = if c.len() >= 2 && c[0] == 0 { t.cstart + 1 } else { t.cstart }; + Some((negative, start, t.cend, t.cend)) + } else { + None + } + }, + } +} + +// --------------------------------------------------------------------------- +// §8.6, §11.2: BIT STRING. +// --------------------------------------------------------------------------- + +/// 2 to the power of a number of unused bits (0 to 7). +pub open spec fn pow2_unused(u: u8) -> nat { + if u == 0 { + 1 + } else if u == 1 { + 2 + } else if u == 2 { + 4 + } else if u == 3 { + 8 + } else if u == 4 { + 16 + } else if u == 5 { + 32 + } else if u == 6 { + 64 + } else { + 128 + } +} + +/// §8.6.2: the contents octets of a primitive bit string: "an initial octet +/// followed by zero, one or more subsequent octets". +pub open spec fn bit_string_contents_ok(c: Seq) -> bool { + &&& c.len() >= 1 + // 8.6.2.2: "The number [of unused bits in the final subsequent octet] + // shall be in the range zero to seven." + &&& c[0] <= 7 + // 8.6.2.3: "If the bitstring is empty, there shall be no subsequent + // octets, and the initial octet shall be zero." + &&& (c.len() == 1 ==> c[0] == 0) + // 11.2.1: "Each unused bit in the final octet of the encoding of a bit + // string value shall be set to zero." The unused bits are the low bits + // of the last octet. + &&& (c.len() >= 2 ==> (c.last() as nat) % pow2_unused(c[0]) == 0) +} + +/// Read a BIT STRING at `pos`. Returns the number of unused bits, the range +/// of the octets that hold the bits, and the next position. +pub open spec fn read_bit_string(b: Seq, pos: nat, end: nat) -> Option<(u8, nat, nat, nat)> { + match read_tlv(b, pos, end) { + None => None, + Some(t) => { + let c = b.subrange(t.cstart as int, t.cend as int); + // 8.6.1 and 10.2: primitive (the constructed form is a BER + // option that DER removes). + if t.tag == universal_primitive(BIT_STRING) && bit_string_contents_ok(c) { + Some((c[0], t.cstart + 1, t.cend, t.cend)) + } else { + None + } + }, + } +} + +// --------------------------------------------------------------------------- +// §8.7, §10.2, §8.8: OCTET STRING, the character string types, NULL. +// --------------------------------------------------------------------------- + +/// Read a primitive value of the universal type `number` whose contents are +/// octets taken as they are: OCTET STRING (§8.7.2), UTF8String, +/// PrintableString, IA5String, UTCTime and GeneralizedTime (their contents +/// are octets; the character and date restrictions are X.509's, see the +/// header). Returns the contents range; the next position is its end. +pub open spec fn read_octets(b: Seq, pos: nat, end: nat, number: u32) -> Option<(nat, nat)> { + match read_tlv(b, pos, end) { + None => None, + Some(t) => { + // 8.7.1, 10.2: "For bitstring, octetstring and restricted + // character string types, the constructed form of encoding + // shall not be used." + if t.tag == universal_primitive(number) { + Some((t.cstart, t.cend)) + } else { + None + } + }, + } +} + +/// An OCTET STRING (§8.7). +pub open spec fn read_octet_string(b: Seq, pos: nat, end: nat) -> Option<(nat, nat)> { + read_octets(b, pos, end, OCTET_STRING) +} + +/// A UTF8String. +pub open spec fn read_utf8_string(b: Seq, pos: nat, end: nat) -> Option<(nat, nat)> { + read_octets(b, pos, end, UTF8_STRING) +} + +/// A PrintableString. +pub open spec fn read_printable_string(b: Seq, pos: nat, end: nat) -> Option<(nat, nat)> { + read_octets(b, pos, end, PRINTABLE_STRING) +} + +/// An IA5String. +pub open spec fn read_ia5_string(b: Seq, pos: nat, end: nat) -> Option<(nat, nat)> { + read_octets(b, pos, end, IA5_STRING) +} + +/// A UTCTime (as octets). +pub open spec fn read_utc_time(b: Seq, pos: nat, end: nat) -> Option<(nat, nat)> { + read_octets(b, pos, end, UTC_TIME) +} + +/// A GeneralizedTime (as octets). +pub open spec fn read_generalized_time(b: Seq, pos: nat, end: nat) -> Option<(nat, nat)> { + read_octets(b, pos, end, GENERALIZED_TIME) +} + +/// Read a NULL at `pos`. Returns the next position. +pub open spec fn read_null(b: Seq, pos: nat, end: nat) -> Option { + match read_tlv(b, pos, end) { + None => None, + Some(t) => { + // 8.8.1: primitive. 8.8.2: "The contents octets shall not + // contain any octets." + if t.tag == universal_primitive(NULL) && t.cstart == t.cend { + Some(t.cend) + } else { + None + } + }, + } +} + +// --------------------------------------------------------------------------- +// §8.19: OBJECT IDENTIFIER. +// --------------------------------------------------------------------------- + +/// §8.19.2: the subidentifiers in `c` from index 0 to its end, each "a +/// series of (one or more) octets" whose bit 8 is one except in the last. +/// Termination: `lemma_series_end_bounds`. +pub open spec fn subidentifiers(c: Seq) -> Option> + decreases c.len(), + via subidentifiers_decreases +{ + if c.len() == 0 { + Some(seq![]) + } else { + match series_end(c, 0, c.len()) { + // The last octet of the series (bit 8 zero) is missing. + None => None, + Some(j) => { + // Bits 7 to 1 of the octets in the series "collectively + // encode the subidentifier". + let v = base128(c.subrange(0, j as int + 1)); + if c[0] == 128 { + // 8.19.2: "The subidentifier shall be encoded in the + // fewest possible octets, that is, the leading octet of + // the subidentifier shall not have the value 80". + None + } else if v >= 0x1_0000_0000_0000_0000 { + // A limit of this project (see the header). + None + } else { + match subidentifiers(c.skip(j as int + 1)) { + None => None, + Some(rest) => Some(seq![v] + rest), + } + } + }, + } + } +} + +/// §8.19.4: "The numerical value of the first subidentifier is derived from +/// the values of the first two object identifier components ... using the +/// formula (X*40) + Y", where X is 0, 1 or 2 (X.660, X.680) and Y is below 40 +/// when X is 0 or 1: so the first two components are recovered from `v`. +pub open spec fn first_components(v: nat) -> (nat, nat) { + if v < 40 { + (0, v) + } else if v < 80 { + (1, (v - 40) as nat) + } else { + (2, (v - 80) as nat) + } +} + +/// The components of an OBJECT IDENTIFIER's contents octets (§8.19.2 to +/// §8.19.5): the first subidentifier gives the first two components, the +/// others one each. +pub open spec fn oid_components(c: Seq) -> Option> { + match subidentifiers(c) { + None => None, + Some(subs) => { + if subs.len() == 0 { + // 8.19.3: N is one less than the number of components, and + // X.680 requires at least two components, so N >= 1. + None + } else { + let (x, y) = first_components(subs[0]); + // 8.19.5: "The numerical value of the ith subidentifier, + // (2 <= i <= N) is that of the (i + 1)th object identifier + // component." + Some(seq![x, y] + subs.skip(1)) + } + }, + } +} + +/// Read an OBJECT IDENTIFIER at `pos`. Returns its components and the next +/// position. +pub open spec fn read_oid(b: Seq, pos: nat, end: nat) -> Option<(Seq, nat)> { + match read_tlv(b, pos, end) { + None => None, + Some(t) => { + // 8.19.1: "The encoding of an object identifier value shall be + // primitive." + if t.tag == universal_primitive(OBJECT_IDENTIFIER) { + match oid_components(b.subrange(t.cstart as int, t.cend as int)) { + Some(arcs) => Some((arcs, t.cend)), + None => None, + } + } else { + None + } + }, + } +} + +// --------------------------------------------------------------------------- +// §8.9, §8.11, §8.12, §8.14: SEQUENCE, SET, tagged types. +// --------------------------------------------------------------------------- + +/// Read a value whose tag is the universal constructed type `number` +/// (SEQUENCE: §8.9.1 "The encoding of a sequence value shall be +/// constructed"; SET: §8.11.1; SEQUENCE OF: §8.10.1; SET OF: §8.12.1). The +/// contents range is the inner range to read with the readers above, and +/// the next position is its end. +pub open spec fn expect_constructed(b: Seq, pos: nat, end: nat, number: u32) -> Option<(nat, nat)> { + match read_tlv(b, pos, end) { + None => None, + Some(t) => { + if t.tag == universal_constructed(number) { + Some((t.cstart, t.cend)) + } else { + None + } + }, + } +} + +/// A SEQUENCE or SEQUENCE OF (§8.9, §8.10). +pub open spec fn expect_sequence(b: Seq, pos: nat, end: nat) -> Option<(nat, nat)> { + expect_constructed(b, pos, end, SEQUENCE) +} + +/// A SET or SET OF (§8.11, §8.12). The order of the components is not +/// checked (§10.3, §11.6 are left to the caller, see the header). +pub open spec fn expect_set(b: Seq, pos: nat, end: nat) -> Option<(nat, nat)> { + expect_constructed(b, pos, end, SET) +} + +/// An OPTIONAL component with a context-specific tag `[number]` (§8.14, §8.9.3: +/// "The encoding of a data value may, but need not, be present for a type +/// which was referenced with the keyword OPTIONAL"): `Some(None)` when no +/// value is left or the next one has another tag, `Some(Some((constructed, +/// cstart, cend)))` when it is there, and `None` when the bytes are not a +/// well-formed value. For an IMPLICIT tag (§8.14.4) the form is that of the +/// underlying type, so `constructed` is returned for the caller to check. +pub open spec fn read_optional_context(b: Seq, pos: nat, end: nat, number: u32) -> Option> { + if pos >= end { + // Nothing is left: the component is absent. + Some(None) + } else { + match read_identifier(b, pos, end) { + None => None, + Some((tag, _)) => { + if tag.class == Class::ContextSpecific && tag.number == number { + match read_tlv(b, pos, end) { + None => None, + Some(t) => Some(Some((tag.constructed, t.cstart, t.cend))), + } + } else { + Some(None) + } + }, + } + } +} + +/// As [`read_optional_context`] for an EXPLICIT tag: "If implicit tagging +/// was not used in the definition of the type, the encoding shall be +/// constructed and the contents octets shall be the complete base encoding" +/// (§8.14.3). The inner range holds one value, read by the caller and +/// followed by [`expect_end`]. +pub open spec fn read_optional_explicit(b: Seq, pos: nat, end: nat, number: u32) -> Option> { + match read_optional_context(b, pos, end, number) { + None => None, + Some(None) => Some(None), + Some(Some((constructed, cstart, cend))) => { + if constructed { + Some(Some((cstart, cend))) + } else { + None + } + }, + } +} + +// --------------------------------------------------------------------------- +// The encoder: the DER encoding of a value (clauses 8, 10 and 11). +// --------------------------------------------------------------------------- + +/// §8.1.2: the identifier octets of `tag` (whose number is below 2^14). +pub open spec fn encode_identifier(tag: Tag) -> Seq { + // 8.1.2.2 a) and b), 8.1.2.5: the class and the primitive/constructed + // bits of the leading octet. + let first = class_bits(tag.class) * 64 + (if tag.constructed { 32u8 } else { 0u8 }); + if tag.number < 31 { + // 8.1.2.2 c): "the number of the tag as a binary integer". + seq![(first + tag.number as u8) as u8] + } else if tag.number < 128 { + // 8.1.2.4: one subsequent octet whose bit 8 is zero (8.1.2.4.2 a) + // and whose bits 7 to 1 are the number. + seq![(first + 31) as u8, tag.number as u8] + } else { + // 8.1.2.4: two subsequent octets, bit 8 set on the first + // (8.1.2.4.2 a), the number base 128 across them (b); the first + // digit is not zero (c) because the number is at least 128. + seq![(first + 31) as u8, (128 + tag.number / 128) as u8, (tag.number % 128) as u8] + } +} + +/// The number of octets after the initial one in the long form of the +/// length `len` in the minimum number of octets (§10.1): the least `w` with +/// `len < 256^w`, for `len < 2^64`. +pub open spec fn length_octets(len: nat) -> nat { + if len < 0x100 { + 1 + } else if len < 0x1_0000 { + 2 + } else if len < 0x100_0000 { + 3 + } else if len < 0x1_0000_0000 { + 4 + } else if len < 0x100_0000_0000 { + 5 + } else if len < 0x1_0000_0000_0000 { + 6 + } else if len < 0x100_0000_0000_0000 { + 7 + } else { + 8 + } +} + +/// §8.1.3, §10.1: the length octets of `len` (below 2^64): the short form +/// for 127 or less, otherwise the long form in the fewest octets. +pub open spec fn encode_length(len: nat) -> Seq { + if len < 128 { + // 8.1.3.4. + seq![len as u8] + } else { + // 8.1.3.5: the initial octet has bit 8 set and bits 7 to 1 the + // number of subsequent octets; the subsequent octets are the length. + let w = length_octets(len); + seq![(128 + w) as u8] + be_bytes(len, w) + } +} + +/// §8.1.1.1: the encoding of a data value with the given tag and contents +/// octets: identifier octets, length octets, contents octets. +pub open spec fn encode_tlv(tag: Tag, contents: Seq) -> Seq { + encode_identifier(tag) + encode_length(contents.len()) + contents +} + +/// What a tag and contents must satisfy to be encoded: a tag number the +/// decoder accepts, and contents short enough for a length of at most 8 +/// octets. +pub open spec fn tlv_ok(tag: Tag, contents: Seq) -> bool { + tag.number <= MAX_TAG_NUMBER && contents.len() < pow256(8) +} + +/// §8.2.2, §11.1: a BOOLEAN. +pub open spec fn encode_boolean(v: bool) -> Seq { + encode_tlv(universal_primitive(BOOLEAN), seq![if v { 0xFFu8 } else { 0x00u8 }]) +} + +/// `s` without its leading zero octets. +pub open spec fn strip_leading_zeros(s: Seq) -> Seq + decreases s.len(), +{ + if s.len() > 0 && s[0] == 0 { + strip_leading_zeros(s.drop_first()) + } else { + s + } +} + +/// §8.3.2, §8.3.3: the contents octets of the INTEGER whose value is the +/// number the octets `m` spell base 256 (a non-negative number, most +/// significant octet first, leading zeros allowed): the octets without +/// leading zeros, one `0x00` octet for the value zero, and a `0x00` octet in +/// front when the first octet has bit 8 set, so the two's complement number +/// is not negative (§8.3.3). +pub open spec fn integer_unsigned_contents(m: Seq) -> Seq { + let s = strip_leading_zeros(m); + if s.len() == 0 { + seq![0u8] + } else if s[0] >= 128 { + seq![0u8] + s + } else { + s + } +} + +/// An INTEGER of a non-negative value given as its magnitude octets. +pub open spec fn encode_integer_unsigned(m: Seq) -> Seq { + encode_tlv(universal_primitive(INTEGER), integer_unsigned_contents(m)) +} + +/// §8.6.2: a primitive BIT STRING with `unused` unused bits (0 to 7) in the +/// final octet of `bits`, whose unused bits are zero (§11.2.1), and which is +/// empty only with `unused` zero (§8.6.2.3). +pub open spec fn encode_bit_string(unused: u8, bits: Seq) -> Seq { + encode_tlv(universal_primitive(BIT_STRING), seq![unused] + bits) +} + +/// §8.7.2: a primitive OCTET STRING. +pub open spec fn encode_octet_string(s: Seq) -> Seq { + encode_tlv(universal_primitive(OCTET_STRING), s) +} + +/// A primitive value of the universal type `number` with contents `s` +/// (UTF8String, PrintableString, IA5String, UTCTime, GeneralizedTime). +pub open spec fn encode_octets(number: u32, s: Seq) -> Seq { + encode_tlv(universal_primitive(number), s) +} + +/// §8.8: NULL, `05 00`. +pub open spec fn encode_null() -> Seq { + encode_tlv(universal_primitive(NULL), seq![]) +} + +/// §8.19.2: the octets of a subidentifier prefix: the digits of `w` base +/// 128, most significant first, each with bit 8 set ("bit 8 of each +/// preceding octet is one"). No leading digit is zero. +pub open spec fn subidentifier_prefix(w: nat) -> Seq + decreases w, +{ + if w == 0 { + seq![] + } else { + subidentifier_prefix(w / 128) + seq![(128 + w % 128) as u8] + } +} + +/// §8.19.2: the octets of the subidentifier `v` in the fewest possible +/// octets: its digits base 128, with bit 8 of the last octet zero. +pub open spec fn encode_subidentifier(v: nat) -> Seq { + subidentifier_prefix(v / 128) + seq![(v % 128) as u8] +} + +/// The octets of a list of subidentifiers, concatenated (§8.19.2). +pub open spec fn encode_subidentifiers(subs: Seq) -> Seq + decreases subs.len(), +{ + if subs.len() == 0 { + seq![] + } else { + encode_subidentifier(subs[0]) + encode_subidentifiers(subs.drop_first()) + } +} + +/// What the components of an object identifier must satisfy to be encoded: +/// at least two; the first is 0, 1 or 2, and the second is below 40 unless +/// the first is 2 (§8.19.4 and X.660); and every subidentifier fits the +/// limit. +pub open spec fn oid_valid(arcs: Seq) -> bool { + &&& arcs.len() >= 2 + &&& arcs[0] <= 2 + &&& arcs[0] < 2 ==> arcs[1] < 40 + &&& 40 * arcs[0] + arcs[1] < 0x1_0000_0000_0000_0000 + &&& forall|i: int| 2 <= i < arcs.len() ==> arcs[i] < 0x1_0000_0000_0000_0000 +} + +/// §8.19.2 to §8.19.5: the contents octets of an object identifier. +pub open spec fn oid_contents(arcs: Seq) -> Seq { + // 8.19.4: the first subidentifier is (X*40) + Y; 8.19.5: the others are + // the remaining components. + encode_subidentifiers(seq![40 * arcs[0] + arcs[1]] + arcs.skip(2)) +} + +/// An OBJECT IDENTIFIER (§8.19.1: primitive). +pub open spec fn encode_oid(arcs: Seq) -> Seq { + encode_tlv(universal_primitive(OBJECT_IDENTIFIER), oid_contents(arcs)) +} + +/// §8.9.1: a SEQUENCE, constructed, with the given contents (the +/// concatenated encodings of its components). +pub open spec fn encode_sequence(contents: Seq) -> Seq { + encode_tlv(universal_constructed(SEQUENCE), contents) +} + +/// §8.11.1: a SET, constructed. +pub open spec fn encode_set(contents: Seq) -> Seq { + encode_tlv(universal_constructed(SET), contents) +} + +/// §8.14: a value with a context-specific tag `[number]` around `contents`: +/// for an EXPLICIT tag the contents are one complete encoding and the form +/// is constructed (§8.14.3); for an IMPLICIT tag they are the base +/// encoding's contents in the base encoding's form (§8.14.4). +pub open spec fn encode_context(number: u32, constructed: bool, contents: Seq) -> Seq { + encode_tlv(context_specific(number, constructed), contents) +} + +/// The `DigestInfo` of RFC 8017 §9.2: `SEQUENCE { digestAlgorithm +/// AlgorithmIdentifier { algorithm OID, parameters NULL }, digest OCTET +/// STRING }`, for the components `arcs` of the algorithm identifier and the +/// digest octets. +pub open spec fn encode_digest_info(arcs: Seq, digest: Seq) -> Seq { + encode_sequence(encode_sequence(encode_oid(arcs) + encode_null()) + encode_octet_string(digest)) +} + +/// Termination of [`subidentifiers`]: the series that ends at `j` leaves a +/// strictly shorter rest. +#[via_fn] +pub proof fn subidentifiers_decreases(c: Seq) { + if c.len() > 0 { + super::termination::lemma_series_end_bounds(c, 0, c.len()); + } +} + +} // verus! diff --git a/crates/clazel-der/src/termination.rs b/crates/clazel-der/src/termination.rs new file mode 100644 index 0000000..9bd978c --- /dev/null +++ b/crates/clazel-der/src/termination.rs @@ -0,0 +1,27 @@ +//! Termination of the recursive subidentifier loop in [`super::spec`]. +//! Verus needs this fact to accept the spec's `decreases` clause (it names a +//! `via` function in `spec.rs`, which only calls this); it says nothing else +//! about the spec. + +#[allow(unused_imports)] +use super::spec::*; +#[allow(unused_imports)] +use vstd::prelude::*; + +verus! { + +/// A series that ends ends at an index in `[i, end)`. +pub proof fn lemma_series_end_bounds(b: Seq, i: nat, end: nat) + ensures + match series_end(b, i, end) { + Some(j) => i <= j && j < end, + None => true, + }, + decreases end - i, +{ + if i < end && b[i as int] >= 128 { + lemma_series_end_bounds(b, i + 1, end); + } +} + +} // verus! diff --git a/crates/clazel-der/tests/conformance_der.rs b/crates/clazel-der/tests/conformance_der.rs new file mode 100644 index 0000000..87ccfd0 --- /dev/null +++ b/crates/clazel-der/tests/conformance_der.rs @@ -0,0 +1,1364 @@ +//! DER conformance (docs/phases/phase-3.md item 3.5a). +//! +//! The proofs show the decoder and encoder equal `spec.rs`, and +//! `independent.rs` shows the spec equal to Kaliski's Layman's Guide on its +//! subset. This file checks the spec against the outside world: +//! +//! 1. Hand-built cases, as Rust literals (no official DER test suite is +//! vendored here: `x509-limbo` is item 3.5b's): valid encodings from X.690 +//! and the Guide that must decode and re-encode to the same bytes, and one +//! case for every rejection rule (indefinite length, a long-form length +//! that fits the short form, a leading zero in a length, INTEGER +//! `00 7F`, BOOLEAN `01`, a BIT STRING with 8 unused bits, an OID with `80` +//! padding, a constructed OCTET STRING, truncated contents, trailing +//! bytes, and others) that must be rejected. +//! 2. A differential test against RustCrypto's `der` crate (0.8.2, default +//! features off): random valid values encoded by `der` must decode to the +//! same value with our readers, our encoders must write the same bytes, +//! and `der` must decode what we wrote. +//! 3. The same on random and mutated bytes. Where we reject and `der` +//! accepts, or the other way round, the difference must be one of the +//! listed categories, each with its reason; any other difference fails +//! the test. The counts of each category are printed. + +use clazel_der::decode::{ + expect_end, read_bit_string, read_boolean, read_generalized_time, read_ia5_string, + read_integer, read_null, read_octet_string, read_oid, read_printable_string, read_tlv, + read_utc_time, read_utf8_string, +}; +use clazel_der::encode::{ + encode_bit_string, encode_boolean, encode_digest_info, encode_integer_unsigned, encode_null, + encode_octet_string, encode_octets, encode_oid, encode_tlv, +}; +use clazel_der::spec::{ + BIT_STRING, BOOLEAN, Class, GENERALIZED_TIME, IA5_STRING, INTEGER, NULL, OBJECT_IDENTIFIER, + OCTET_STRING, PRINTABLE_STRING, Tag, UTC_TIME, UTF8_STRING, +}; + +use der::asn1::{ + AnyRef, BitString, Ia5StringRef, Int, Null, ObjectIdentifier, OctetString, PrintableStringRef, + UtcTime, Utf8StringRef, +}; +use der::{Class as DerClass, Decode, Encode, Tag as DerTag, TagNumber, Tagged}; + +// --------------------------------------------------------------------------- +// Helpers without indexing or unchecked arithmetic. +// --------------------------------------------------------------------------- + +fn at(b: &[u8], i: usize) -> u8 { + b.get(i).copied().unwrap_or(0) +} + +fn sub(b: &[u8], from: usize, to: usize) -> Vec { + b.get(from..to).map(<[u8]>::to_vec).unwrap_or_default() +} + +/// A small deterministic generator (xorshift), so failures reproduce. +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } + fn below(&mut self, n: usize) -> usize { + usize::try_from( + self.next() + .checked_rem(u64::try_from(n).unwrap_or(1)) + .unwrap_or(0), + ) + .unwrap_or(0) + } + fn byte(&mut self) -> u8 { + u8::try_from(self.next() & 0xFF).unwrap_or(0) + } + fn bytes(&mut self, n: usize) -> Vec { + (0..n).map(|_| self.byte()).collect() + } + fn coin(&mut self) -> bool { + self.below(2) == 1 + } +} + +// --------------------------------------------------------------------------- +// The values compared, and the two sides' readers and writers. +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Kind { + Bool, + Int, + Bits, + Octets, + Null, + Oid, + Utf8, + Printable, + Ia5, + Utc, + Any, +} + +const KINDS: [Kind; 11] = [ + Kind::Bool, + Kind::Int, + Kind::Bits, + Kind::Octets, + Kind::Null, + Kind::Oid, + Kind::Utf8, + Kind::Printable, + Kind::Ia5, + Kind::Utc, + Kind::Any, +]; + +/// A decoded value in a form both sides can produce. +#[derive(Debug, Clone, PartialEq, Eq)] +enum Val { + Bool(bool), + /// The contents octets of an INTEGER. + Int(Vec), + Bits(u8, Vec), + Octets(Vec), + Null, + Oid(Vec), + Str(Vec), + /// class bits, constructed, number, contents + Any(u8, bool, u32, Vec), +} + +fn our_tag_bits(tag: Tag) -> u8 { + match tag.class { + Class::Universal => 0, + Class::Application => 1, + Class::ContextSpecific => 2, + Class::Private => 3, + } +} + +/// The whole input must be one value: nothing may follow it. +fn whole(next: usize, b: &[u8]) -> bool { + expect_end(next, b.len()) +} + +fn ours_decode(kind: Kind, b: &[u8]) -> Option { + let len = b.len(); + match kind { + Kind::Bool => { + let (v, next) = read_boolean(b, 0, len)?; + whole(next, b).then_some(Val::Bool(v)) + } + Kind::Int => { + let (_, _, _, next) = read_integer(b, 0, len)?; + let t = read_tlv(b, 0, len)?; + whole(next, b).then(|| Val::Int(sub(b, t.cstart, t.cend))) + } + Kind::Bits => { + let (unused, s, e, next) = read_bit_string(b, 0, len)?; + whole(next, b).then(|| Val::Bits(unused, sub(b, s, e))) + } + Kind::Octets => { + let (s, e) = read_octet_string(b, 0, len)?; + whole(e, b).then(|| Val::Octets(sub(b, s, e))) + } + Kind::Null => { + let next = read_null(b, 0, len)?; + whole(next, b).then_some(Val::Null) + } + Kind::Oid => { + let (arcs, next) = read_oid(b, 0, len)?; + whole(next, b).then_some(Val::Oid(arcs)) + } + Kind::Utf8 | Kind::Printable | Kind::Ia5 | Kind::Utc => { + let (s, e) = match kind { + Kind::Utf8 => read_utf8_string(b, 0, len)?, + Kind::Printable => read_printable_string(b, 0, len)?, + Kind::Ia5 => read_ia5_string(b, 0, len)?, + _ => read_utc_time(b, 0, len)?, + }; + whole(e, b).then(|| Val::Str(sub(b, s, e))) + } + Kind::Any => { + let t = read_tlv(b, 0, len)?; + whole(t.cend, b).then(|| { + Val::Any( + our_tag_bits(t.tag), + t.tag.constructed, + t.tag.number, + sub(b, t.cstart, t.cend), + ) + }) + } + } +} + +fn ours_encode(val: &Val, kind: Kind) -> Vec { + match (val, kind) { + (Val::Bool(v), _) => encode_boolean(*v), + (Val::Int(c), _) => { + if at(c, 0) < 128 { + encode_integer_unsigned(c) + } else { + encode_tlv( + Tag { + class: Class::Universal, + constructed: false, + number: INTEGER, + }, + c, + ) + } + } + (Val::Bits(u, b), _) => encode_bit_string(*u, b), + (Val::Octets(s), _) => encode_octet_string(s), + (Val::Null, _) => encode_null(), + (Val::Oid(arcs), _) => encode_oid(arcs), + (Val::Str(s), Kind::Utf8) => encode_octets(UTF8_STRING, s), + (Val::Str(s), Kind::Printable) => encode_octets(PRINTABLE_STRING, s), + (Val::Str(s), Kind::Ia5) => encode_octets(IA5_STRING, s), + (Val::Str(s), _) => encode_octets(UTC_TIME, s), + (Val::Any(class, constructed, number, c), _) => { + let class = match class { + 1 => Class::Application, + 2 => Class::ContextSpecific, + 3 => Class::Private, + _ => Class::Universal, + }; + encode_tlv( + Tag { + class, + constructed: *constructed, + number: *number, + }, + c, + ) + } + } +} + +fn theirs_decode(kind: Kind, b: &[u8]) -> Option { + match kind { + Kind::Bool => bool::from_der(b).ok().map(Val::Bool), + Kind::Int => Int::from_der(b) + .ok() + .map(|i| Val::Int(i.as_bytes().to_vec())), + Kind::Bits => { + let bs = BitString::from_der(b).ok()?; + Some(Val::Bits(bs.unused_bits(), bs.raw_bytes().to_vec())) + } + Kind::Octets => OctetString::from_der(b) + .ok() + .map(|o| Val::Octets(o.as_bytes().to_vec())), + Kind::Null => Null::from_der(b).ok().map(|_| Val::Null), + Kind::Oid => { + let oid = ObjectIdentifier::<39>::from_der(b).ok()?; + Some(Val::Oid(oid.arcs().map(u64::from).collect())) + } + Kind::Utf8 => Utf8StringRef::from_der(b) + .ok() + .map(|s| Val::Str(s.as_str().as_bytes().to_vec())), + Kind::Printable => PrintableStringRef::from_der(b) + .ok() + .map(|s| Val::Str(s.as_str().as_bytes().to_vec())), + Kind::Ia5 => Ia5StringRef::from_der(b) + .ok() + .map(|s| Val::Str(s.as_str().as_bytes().to_vec())), + Kind::Utc => { + // `der` parses a UTCTime into a date and writes it back canonically. + let t = UtcTime::from_der(b).ok()?; + let again = t.to_der().ok()?; + Some(Val::Str( + again.get(2..).map(<[u8]>::to_vec).unwrap_or_default(), + )) + } + Kind::Any => { + let any = AnyRef::from_der(b).ok()?; + let tag = any.tag(); + let class = match tag.class() { + DerClass::Universal => 0, + DerClass::Application => 1, + DerClass::ContextSpecific => 2, + DerClass::Private => 3, + }; + Some(Val::Any( + class, + tag.is_constructed(), + tag.number().value(), + any.value().to_vec(), + )) + } + } +} + +// --------------------------------------------------------------------------- +// Random valid values, written by `der`. +// --------------------------------------------------------------------------- + +const PRINTABLE: &[u8] = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 '()+,-./:=?"; + +fn edge_len(r: &mut Rng) -> usize { + let edges = [0usize, 1, 2, 126, 127, 128, 129, 255, 256, 257, 1000]; + if r.below(3) == 0 { + edges.get(r.below(edges.len())).copied().unwrap_or(0) + } else { + r.below(300) + } +} + +fn random_arc(r: &mut Rng) -> u32 { + let edges = [ + 0u32, + 1, + 39, + 40, + 127, + 128, + 16383, + 16384, + 2_097_151, + 2_097_152, + u32::MAX, + ]; + if r.coin() { + edges.get(r.below(edges.len())).copied().unwrap_or(0) + } else { + u32::try_from(r.next() & 0xFFFF_FFFF).unwrap_or(0) >> r.below(32) + } +} + +/// A random value of `kind` and its encoding by `der`. `None` where `der` +/// declines to build one (then the case is skipped). +fn generate(kind: Kind, r: &mut Rng) -> Option<(Vec, Val)> { + match kind { + Kind::Bool => { + let v = r.coin(); + Some((v.to_der().ok()?, Val::Bool(v))) + } + Kind::Int => { + let n = r.below(20).saturating_add(1); + let mut bytes = r.bytes(n); + if r.below(3) == 0 { + // Redundant leading octets, which `der` canonicalises away. + let pad = if r.coin() { 0x00 } else { 0xFF }; + let n = r.below(3); + bytes.splice(0..0, std::iter::repeat_n(pad, n)); + } + // `Int::new` keeps redundant octets, which DER forbids: strip them. + while bytes.len() >= 2 + && ((at(&bytes, 0) == 0x00 && at(&bytes, 1) < 0x80) + || (at(&bytes, 0) == 0xFF && at(&bytes, 1) >= 0x80)) + { + bytes.remove(0); + } + let i = Int::new(&bytes).ok()?; + Some((i.to_der().ok()?, Val::Int(i.as_bytes().to_vec()))) + } + Kind::Bits => { + let n = r.below(40); + let mut bytes = r.bytes(n); + let unused = if n == 0 { + 0 + } else { + u8::try_from(r.below(8)).unwrap_or(0) + }; + if let Some(last) = bytes.last_mut() { + *last &= 0xFFu8 << unused; + } + let bs = BitString::new(unused, bytes.clone()).ok()?; + Some((bs.to_der().ok()?, Val::Bits(unused, bytes))) + } + Kind::Octets => { + let n = edge_len(r); + let bytes = r.bytes(n); + let o = OctetString::new(bytes.clone()).ok()?; + Some((o.to_der().ok()?, Val::Octets(bytes))) + } + Kind::Null => Some((Null.to_der().ok()?, Val::Null)), + Kind::Oid => { + let first = u32::try_from(r.below(3)).unwrap_or(0); + let second = if first < 2 { + u32::try_from(r.below(40)).unwrap_or(0) + } else { + random_arc(r) >> 2 + }; + let mut arcs = vec![first, second]; + for _ in 0..r.below(9) { + arcs.push(random_arc(r)); + } + let oid = ObjectIdentifier::<39>::from_arcs(arcs.iter().copied()).ok()?; + Some(( + oid.to_der().ok()?, + Val::Oid(arcs.iter().map(|a| u64::from(*a)).collect()), + )) + } + Kind::Utf8 => { + let pool = [ + 'a', + 'Z', + '0', + ' ', + '\u{e9}', + '\u{20ac}', + '\u{1f600}', + '\u{7ff}', + '\u{800}', + ]; + let s: String = (0..edge_len(r).min(120)) + .map(|_| pool.get(r.below(pool.len())).copied().unwrap_or('a')) + .collect(); + let u = Utf8StringRef::new(&s).ok()?; + Some((u.to_der().ok()?, Val::Str(s.into_bytes()))) + } + Kind::Printable => { + let s: Vec = (0..edge_len(r).min(120)) + .map(|_| { + PRINTABLE + .get(r.below(PRINTABLE.len())) + .copied() + .unwrap_or(b'a') + }) + .collect(); + let p = PrintableStringRef::new(&s).ok()?; + Some((p.to_der().ok()?, Val::Str(s))) + } + Kind::Ia5 => { + let s: Vec = (0..edge_len(r).min(120)).map(|_| r.byte() & 0x7F).collect(); + let p = Ia5StringRef::new(&s).ok()?; + Some((p.to_der().ok()?, Val::Str(s))) + } + Kind::Utc => { + let two = |v: usize| format!("{v:02}"); + let s = format!( + "{}{}{}{}{}{}Z", + two(r.below(100)), + two(r.below(12).saturating_add(1)), + two(r.below(28).saturating_add(1)), + two(r.below(24)), + two(r.below(60)), + two(r.below(60)) + ); + let mut enc = vec![0x17, 0x0D]; + enc.extend_from_slice(s.as_bytes()); + let t = UtcTime::from_der(&enc).ok()?; + Some((t.to_der().ok()?, Val::Str(s.into_bytes()))) + } + Kind::Any => { + let number = { + let edges = [0u32, 1, 30, 31, 32, 127, 128, 255, 16383]; + if r.coin() { + edges.get(r.below(edges.len())).copied().unwrap_or(0) + } else { + u32::try_from(r.below(16384)).unwrap_or(0) + } + }; + let constructed = r.coin(); + let (tag, class) = match r.below(3) { + 0 => (TagNumber(number).application(constructed), 1u8), + 1 => (TagNumber(number).context_specific(constructed), 2), + _ => (TagNumber(number).private(constructed), 3), + }; + let n = edge_len(r); + let contents = r.bytes(n); + let any = AnyRef::new(tag, &contents).ok()?; + Some(( + any.to_der().ok()?, + Val::Any(class, constructed, number, contents), + )) + } + } +} + +// --------------------------------------------------------------------------- +// Mutations. +// --------------------------------------------------------------------------- + +fn mutate(b: &[u8], r: &mut Rng) -> Vec { + let mut v = b.to_vec(); + match r.below(9) { + 0 => { + let i = r.below(v.len().max(1)); + if let Some(x) = v.get_mut(i) { + *x = r.byte(); + } + } + 1 => { + let i = r.below(v.len().max(1)); + let bit = 1u8 << r.below(8); + if let Some(x) = v.get_mut(i) { + *x ^= bit; + } + } + 2 => v.truncate(r.below(v.len().saturating_add(1))), + 3 => { + let n = r.below(4).saturating_add(1); + let extra = r.bytes(n); + v.extend(extra); + } + 4 => { + let i = r.below(v.len().saturating_add(1)); + v.insert(i, r.byte()); + } + 5 => { + if !v.is_empty() { + let i = r.below(v.len()); + v.remove(i); + } + } + 6 => { + // A tricky length octet in place of the second octet. + let tricky = [0x80u8, 0xFF, 0x81, 0x82, 0x84, 0x7F, 0x00]; + let t = tricky.get(r.below(tricky.len())).copied().unwrap_or(0); + if let Some(x) = v.get_mut(1) { + *x = t; + } + } + 7 => { + // A tricky first octet. + let tricky = [0x1Fu8, 0x00, 0x3F, 0x5F, 0xFF, 0x20, 0x24, 0xA0]; + let t = tricky.get(r.below(tricky.len())).copied().unwrap_or(0); + if let Some(x) = v.get_mut(0) { + *x = t; + } + } + _ => { + let i = r.below(v.len().max(1)); + let j = r.below(v.len().max(1)); + let (a, c) = (at(&v, i), at(&v, j)); + if let Some(x) = v.get_mut(i) { + *x = c; + } + if let Some(x) = v.get_mut(j) { + *x = a; + } + } + } + v +} + +/// Why we accept a value that `der` rejects, if the difference is explained. +fn we_accept_they_reject(kind: Kind, val: &Val, encoded: &[u8]) -> Option<&'static str> { + match (kind, val) { + (Kind::Utf8, Val::Str(s)) if std::str::from_utf8(s).is_err() => { + Some("UTF8String contents are not checked here (X.509's, item 3.5b)") + } + (Kind::Printable, Val::Str(s)) if s.iter().any(|c| !PRINTABLE.contains(c)) => { + Some("PrintableString characters are not checked here (X.509's, item 3.5b)") + } + (Kind::Ia5, Val::Str(s)) if s.iter().any(|c| *c >= 0x80) => { + Some("IA5String characters are not checked here (X.509's, item 3.5b)") + } + (Kind::Utc, Val::Str(_)) => { + Some("UTCTime date format is not checked here (RFC 5280 §4.1.2.5, item 3.5b)") + } + (Kind::Oid, Val::Oid(arcs)) if arcs.iter().any(|a| *a > u64::from(u32::MAX)) => { + Some("an arc above 2^32 (ours are u64, `const-oid`'s are u32)") + } + (Kind::Oid, Val::Oid(_)) if encoded.len() > 41 => { + Some("an OID of more than 39 contents octets (`const-oid`'s buffer)") + } + (Kind::Oid, Val::Oid(arcs)) if arcs.get(1).is_some_and(|a| *a > 39) => { + Some("a second arc above 39 (`const-oid` refuses it, X.660 allows it under the root 2)") + } + (Kind::Any, Val::Any(0, _, _, _)) => Some("a universal tag `der` has no variant for"), + _ => None, + } +} + +/// Whether any subidentifier of an OID's contents begins with the padding +/// octet 0x80 (X.690 8.19.2). +fn oid_padded(encoded: &[u8]) -> bool { + let header = if at(encoded, 1) < 0x80 { + 2 + } else { + 2usize.saturating_add(usize::from(at(encoded, 1) & 0x7F)) + }; + let mut start = true; + for c in encoded.iter().skip(header) { + if start && *c == 0x80 { + return true; + } + start = *c < 0x80; + } + false +} + +/// Why we reject something `der` accepts, if the difference is explained. +fn we_reject_they_accept(kind: Kind, val: &Val, encoded: &[u8]) -> Option<&'static str> { + match (kind, val) { + (Kind::Any, Val::Any(_, _, number, _)) if *number > 16383 => { + Some("a tag number of 2^14 or more (our stated limit)") + } + (Kind::Bits, Val::Bits(unused, bytes)) + if *unused > 0 + && bytes + .last() + .is_some_and(|b| *b & ((1u8 << *unused).wrapping_sub(1)) != 0) => + { + Some("BIT STRING padding bits that are not zero (X.690 11.2.1; `der` does not check)") + } + (Kind::Oid, Val::Oid(_)) if oid_padded(encoded) => { + Some("an OID subidentifier padded with 80 (X.690 8.19.2; `der` does not check)") + } + _ => None, + } +} + +/// Why both accept but read different values, if explained. +fn both_accept_differ(kind: Kind, ours: &Val) -> Option<&'static str> { + match (kind, ours) { + (Kind::Oid, Val::Oid(arcs)) if arcs.iter().any(|a| *a > u64::from(u32::MAX)) => { + Some("an arc above 2^32: `const-oid` truncates it silently, we read it as a u64") + } + _ => None, + } +} + +// --------------------------------------------------------------------------- +// Suite 1: hand-built cases. +// --------------------------------------------------------------------------- + +/// 04 81 C9 followed by 201 zero octets. +static LONG_201: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + let mut v = vec![0x04, 0x81, 0xC9]; + v.extend(std::iter::repeat_n(0u8, 201)); + v +}); + +struct Hand<'a> { + name: &'static str, + kind: Kind, + bytes: &'a [u8], + /// The value it must decode to, or `None` if it must be rejected. + expected: Option, +} + +fn hand_cases() -> Vec> { + let v = |name, kind, bytes, expected| Hand { + name, + kind, + bytes, + expected, + }; + vec![ + // --- valid: X.690 and Kaliski's examples. + v( + "X.690 8.2.2 BOOLEAN TRUE 01 01 FF", + Kind::Bool, + &[0x01, 0x01, 0xFF], + Some(Val::Bool(true)), + ), + v( + "BOOLEAN FALSE 01 01 00", + Kind::Bool, + &[0x01, 0x01, 0x00], + Some(Val::Bool(false)), + ), + v( + "X.690 8.8 NULL 05 00", + Kind::Null, + &[0x05, 0x00], + Some(Val::Null), + ), + v( + "Guide 5.7 INTEGER 0", + Kind::Int, + &[0x02, 0x01, 0x00], + Some(Val::Int(vec![0x00])), + ), + v( + "Guide 5.7 INTEGER 127", + Kind::Int, + &[0x02, 0x01, 0x7F], + Some(Val::Int(vec![0x7F])), + ), + v( + "Guide 5.7 INTEGER 128", + Kind::Int, + &[0x02, 0x02, 0x00, 0x80], + Some(Val::Int(vec![0x00, 0x80])), + ), + v( + "Guide 5.7 INTEGER 256", + Kind::Int, + &[0x02, 0x02, 0x01, 0x00], + Some(Val::Int(vec![0x01, 0x00])), + ), + v( + "Guide 5.7 INTEGER -128", + Kind::Int, + &[0x02, 0x01, 0x80], + Some(Val::Int(vec![0x80])), + ), + v( + "Guide 5.7 INTEGER -129", + Kind::Int, + &[0x02, 0x02, 0xFF, 0x7F], + Some(Val::Int(vec![0xFF, 0x7F])), + ), + v( + "X.690 8.6.4.2 BIT STRING '0A3B5F291CD'H", + Kind::Bits, + &[0x03, 0x07, 0x04, 0x0A, 0x3B, 0x5F, 0x29, 0x1C, 0xD0], + Some(Val::Bits(4, vec![0x0A, 0x3B, 0x5F, 0x29, 0x1C, 0xD0])), + ), + v( + "Guide 5.4 BIT STRING 011011100101110111", + Kind::Bits, + &[0x03, 0x04, 0x06, 0x6E, 0x5D, 0xC0], + Some(Val::Bits(6, vec![0x6E, 0x5D, 0xC0])), + ), + v( + "empty BIT STRING 03 01 00", + Kind::Bits, + &[0x03, 0x01, 0x00], + Some(Val::Bits(0, vec![])), + ), + v( + "Guide 5.10 OCTET STRING", + Kind::Octets, + &[0x04, 0x08, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF], + Some(Val::Octets(vec![ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + ])), + ), + v( + "X.690 8.19.5 OID {2 999 3}", + Kind::Oid, + &[0x06, 0x03, 0x88, 0x37, 0x03], + Some(Val::Oid(vec![2, 999, 3])), + ), + v( + "Guide 5.9 OID {1 2 840 113549}", + Kind::Oid, + &[0x06, 0x06, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D], + Some(Val::Oid(vec![1, 2, 840, 113_549])), + ), + v( + "X.690 8.1.3.5 length 201 in the long form", + Kind::Octets, + LONG_201.as_slice(), + Some(Val::Octets(vec![0; 201])), + ), + v( + "Guide 5.6 IA5String", + Kind::Ia5, + &[0x16, 0x03, 0x61, 0x62, 0x63], + Some(Val::Str(b"abc".to_vec())), + ), + v( + "PrintableString", + Kind::Printable, + &[0x13, 0x02, 0x55, 0x53], + Some(Val::Str(b"US".to_vec())), + ), + v( + "UTF8String", + Kind::Utf8, + &[0x0C, 0x03, 0xE2, 0x82, 0xAC], + Some(Val::Str("\u{20ac}".as_bytes().to_vec())), + ), + v( + "Guide 5.17 UTCTime 910506234540Z", + Kind::Utc, + &[ + 0x17, 0x0D, 0x39, 0x31, 0x30, 0x35, 0x30, 0x36, 0x32, 0x33, 0x34, 0x35, 0x34, 0x30, + 0x5A, + ], + Some(Val::Str(b"910506234540Z".to_vec())), + ), + v( + "context [2] constructed A2 00", + Kind::Any, + &[0xA2, 0x00], + Some(Val::Any(2, true, 2, vec![])), + ), + v( + "high tag number 31 (1F 1F 00)", + Kind::Any, + &[0x5F, 0x1F, 0x00], + Some(Val::Any(1, false, 31, vec![])), + ), + v( + "high tag number 16383 (BF FF 7F 00)", + Kind::Any, + &[0xBF, 0xFF, 0x7F, 0x00], + Some(Val::Any(2, true, 16383, vec![])), + ), + // --- rejected: every rule. + v( + "X.690 8.1.3.6 indefinite length (30 80 .. 00 00)", + Kind::Any, + &[0x30, 0x80, 0x02, 0x01, 0x00, 0x00, 0x00], + None, + ), + v( + "indefinite length of an OCTET STRING", + Kind::Octets, + &[0x24, 0x80, 0x04, 0x01, 0x00, 0x00, 0x00], + None, + ), + v( + "X.690 8.1.3.5 c) reserved length octet FF", + Kind::Octets, + &[0x04, 0xFF, 0x00], + None, + ), + v( + "10.1 length 38 in the long form (81 26)", + Kind::Octets, + &[0x04, 0x81, 0x26], + None, + ), + v( + "10.1 long form that fits the short form, 0 (81 00)", + Kind::Null, + &[0x05, 0x81, 0x00], + None, + ), + v( + "10.1 length with a leading zero octet (82 00 C9)", + Kind::Octets, + &[0x04, 0x82, 0x00, 0xC9], + None, + ), + v( + "10.1 length 201 in three octets (82 00 C9)", + Kind::Any, + &[0x04, 0x82, 0x00, 0xC9], + None, + ), + v( + "8.3.2 INTEGER 00 7F", + Kind::Int, + &[0x02, 0x02, 0x00, 0x7F], + None, + ), + v( + "8.3.2 INTEGER 00 00", + Kind::Int, + &[0x02, 0x02, 0x00, 0x00], + None, + ), + v( + "8.3.2 INTEGER FF 80", + Kind::Int, + &[0x02, 0x02, 0xFF, 0x80], + None, + ), + v( + "8.3.2 INTEGER FF FF", + Kind::Int, + &[0x02, 0x02, 0xFF, 0xFF], + None, + ), + v( + "8.3.1 INTEGER with no contents", + Kind::Int, + &[0x02, 0x00], + None, + ), + v( + "11.1 BOOLEAN TRUE as 01", + Kind::Bool, + &[0x01, 0x01, 0x01], + None, + ), + v( + "11.1 BOOLEAN TRUE as 7F", + Kind::Bool, + &[0x01, 0x01, 0x7F], + None, + ), + v( + "8.2.1 BOOLEAN with two octets", + Kind::Bool, + &[0x01, 0x02, 0xFF, 0xFF], + None, + ), + v( + "8.2.1 BOOLEAN with no contents", + Kind::Bool, + &[0x01, 0x00], + None, + ), + v( + "8.6.2.2 BIT STRING with 8 unused bits", + Kind::Bits, + &[0x03, 0x02, 0x08, 0x00], + None, + ), + v( + "8.6.2.3 empty BIT STRING with 1 unused bit", + Kind::Bits, + &[0x03, 0x01, 0x01], + None, + ), + v( + "8.6.2.1 BIT STRING with no contents", + Kind::Bits, + &[0x03, 0x00], + None, + ), + v( + "11.2.1 BIT STRING padding bits not zero", + Kind::Bits, + &[0x03, 0x04, 0x06, 0x6E, 0x5D, 0xE0], + None, + ), + v( + "10.2 constructed BIT STRING", + Kind::Bits, + &[ + 0x23, 0x09, 0x03, 0x03, 0x00, 0x6E, 0x5D, 0x03, 0x02, 0x06, 0xC0, + ], + None, + ), + v( + "8.8.2 NULL with contents", + Kind::Null, + &[0x05, 0x01, 0x00], + None, + ), + v( + "8.19.2 OID subidentifier padded with 80", + Kind::Oid, + &[0x06, 0x04, 0x2A, 0x80, 0x81, 0x01], + None, + ), + v( + "8.19.2 OID whose last octet has bit 8 set", + Kind::Oid, + &[0x06, 0x02, 0x2A, 0x86], + None, + ), + v( + "8.19.3 OID with no subidentifier", + Kind::Oid, + &[0x06, 0x00], + None, + ), + v( + "8.19.1 constructed OBJECT IDENTIFIER", + Kind::Oid, + &[0x26, 0x01, 0x2A], + None, + ), + v( + "10.2 constructed OCTET STRING", + Kind::Octets, + &[0x24, 0x06, 0x04, 0x01, 0x01, 0x04, 0x01, 0x02], + None, + ), + v( + "10.2 constructed UTF8String", + Kind::Utf8, + &[0x2C, 0x03, 0x0C, 0x01, 0x61], + None, + ), + v( + "8.1.1.1 truncated contents", + Kind::Octets, + &[0x04, 0x05, 0x01, 0x02], + None, + ), + v( + "8.1.1.1 truncated length octets", + Kind::Octets, + &[0x04, 0x82, 0x01], + None, + ), + v("8.1.1.1 identifier only", Kind::Any, &[0x04], None), + v("8.1.1.1 empty input", Kind::Any, &[], None), + v( + "trailing bytes after a value", + Kind::Null, + &[0x05, 0x00, 0x00], + None, + ), + v( + "trailing bytes after an OCTET STRING", + Kind::Octets, + &[0x04, 0x01, 0x00, 0xFF], + None, + ), + v( + "8.1.2.2 high form for tag 30 (5F 1E)", + Kind::Any, + &[0x5F, 0x1E, 0x00], + None, + ), + v( + "8.1.2.4.2 c) high tag number with a leading 80 digit", + Kind::Any, + &[0x5F, 0x80, 0x1F, 0x00], + None, + ), + v( + "8.1.2.4.2 a) high tag number never ends", + Kind::Any, + &[0x5F, 0x81], + None, + ), + v( + "tag number 16384, past this crate's limit (stated)", + Kind::Any, + &[0x5F, 0x81, 0x80, 0x00, 0x00], + None, + ), + v( + "wrong tag: INTEGER read as OCTET STRING", + Kind::Octets, + &[0x02, 0x01, 0x00], + None, + ), + v( + "wrong class: [0] primitive read as OCTET STRING", + Kind::Octets, + &[0x84, 0x00], + None, + ), + ] +} + +#[test] +fn hand_built_cases() { + let cases = hand_cases(); + let mut failures = Vec::new(); + let mut failed = 0usize; + let mut valid = 0usize; + let mut theirs_differs = Vec::new(); + for c in &cases { + let before = failures.len(); + let got = ours_decode(c.kind, c.bytes); + match (&c.expected, &got) { + (None, None) => {} + (Some(e), Some(g)) if e == g => { + valid = valid.saturating_add(1); + // It must re-encode to the same bytes (DER canonicity). + if ours_encode(g, c.kind) != c.bytes { + failures.push(format!("{}: does not re-encode to the same bytes", c.name)); + } + } + (None, Some(g)) => { + failures.push(format!("{}: must be rejected but decoded to {g:?}", c.name)) + } + (Some(e), got) => failures.push(format!("{}: expected {e:?}, got {got:?}", c.name)), + } + // Note what `der` does with the same bytes. + if theirs_decode(c.kind, c.bytes) != c.expected { + theirs_differs.push(c.name); + } + if failures.len() != before { + failed = failed.saturating_add(1); + } + } + println!( + "clazel-conformance: suite=\"DER hand-built cases (X.690, Kaliski, one per rule)\" pass={} xfail=0 fail={failed} total={}", + cases.len().saturating_sub(failed), + cases.len() + ); + println!( + "{valid} valid cases; `der` 0.8.2 differs on {} of {}: {theirs_differs:?}", + theirs_differs.len(), + cases.len() + ); + assert!(failures.is_empty(), "{}", failures.join("\n")); + // `der` is itself a DER decoder: the only cases it treats differently + // are `der`'s own limits and unchecked rules, which are, in order: an + // OID whose second arc is above 39 under the root 2 (`const-oid`), BIT + // STRING padding bits that are not zero (11.2.1), an OID subidentifier + // padded with 80 (8.19.2), and a tag number above our stated limit. + assert_eq!( + theirs_differs, + [ + "X.690 8.19.5 OID {2 999 3}", + "11.2.1 BIT STRING padding bits not zero", + "8.19.2 OID subidentifier padded with 80", + "tag number 16384, past this crate's limit (stated)" + ] + ); +} + +/// RFC 8017 §9.2 Note 1, the `DigestInfo` prefixes, through the encoder, for +/// the three digests item 3.5c needs. +#[test] +fn digest_info_prefixes() { + let oids: [(&[u64], usize, &[u8]); 3] = [ + ( + &[2, 16, 840, 1, 101, 3, 4, 2, 1], + 32, + &[ + 0x30, 0x31, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, + 0x01, 0x05, 0x00, 0x04, 0x20, + ], + ), + ( + &[2, 16, 840, 1, 101, 3, 4, 2, 2], + 48, + &[ + 0x30, 0x41, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, + 0x02, 0x05, 0x00, 0x04, 0x30, + ], + ), + ( + &[2, 16, 840, 1, 101, 3, 4, 2, 3], + 64, + &[ + 0x30, 0x51, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, + 0x03, 0x05, 0x00, 0x04, 0x40, + ], + ), + ]; + let mut failed = 0usize; + for (arcs, n, prefix) in oids { + let digest: Vec = (0..n) + .map(|i| u8::try_from(i & 0xFF).unwrap_or(0)) + .collect(); + let mut expected = prefix.to_vec(); + expected.extend_from_slice(&digest); + if encode_digest_info(arcs, &digest) != expected { + failed = failed.saturating_add(1); + } + } + println!( + "clazel-conformance: suite=\"RFC 8017 9.2 DigestInfo prefixes (SHA-256, SHA-384, SHA-512)\" pass={} xfail=0 fail={failed} total=3", + 3usize.saturating_sub(failed) + ); + assert_eq!(failed, 0); +} + +/// Rounds of the two random suites; Miri (an interpreter, about a thousand +/// times slower) runs a hundredth of them. +const VALID_ROUNDS: usize = if cfg!(miri) { 60 } else { 6000 }; +const MUTATED_ROUNDS: usize = if cfg!(miri) { 600 } else { 60_000 }; + +// --------------------------------------------------------------------------- +// Suites 2 and 3: the differential against `der`. +// --------------------------------------------------------------------------- + +#[test] +fn differential_valid_values() { + let mut r = Rng(0x9E37_79B9_7F4A_7C15); + let mut failures = Vec::new(); + let mut cases = 0usize; + let mut failed = 0usize; + let mut per_kind = [0usize; 11]; + for round in 0..VALID_ROUNDS { + let kind = KINDS + .get(round % KINDS.len()) + .copied() + .unwrap_or(Kind::Null); + let Some((their_bytes, val)) = generate(kind, &mut r) else { + continue; + }; + cases = cases.saturating_add(1); + if let Some(n) = per_kind.get_mut(round % KINDS.len()) { + *n = n.saturating_add(1); + } + let before = failures.len(); + // 1. `der` writes, we read. + let ours = ours_decode(kind, &their_bytes); + if ours.as_ref() != Some(&val) { + failures.push(format!( + "{kind:?}: we read {ours:?} from {their_bytes:02x?}, expected {val:?}" + )); + } + // 2. We write: the same bytes (DER has one encoding). + let mine = ours_encode(&val, kind); + if mine != their_bytes { + failures.push(format!( + "{kind:?}: we write {mine:02x?}, `der` writes {their_bytes:02x?} for {val:?}" + )); + } + // 3. `der` reads what we wrote. + let back = theirs_decode(kind, &mine); + if back.as_ref() != Some(&val) { + failures.push(format!( + "{kind:?}: `der` reads {back:?} from {mine:02x?}, expected {val:?}" + )); + } + if failures.len() != before { + failed = failed.saturating_add(1); + } + } + println!( + "clazel-conformance: suite=\"der 0.8.2 differential, random valid values (der writes, we read; we write, der reads)\" pass={} xfail=0 fail={failed} total={cases}", + cases.saturating_sub(failed) + ); + println!("valid values per kind: {per_kind:?}"); + assert!( + cases > VALID_ROUNDS / 6 * 5, + "only {cases} values were generated" + ); + assert!( + failures.is_empty(), + "{}", + failures + .iter() + .take(10) + .cloned() + .collect::>() + .join("\n") + ); +} + +#[test] +fn differential_mutated_bytes() { + let mut r = Rng(0xD1B5_4A32_D192_ED03); + let mut failures = Vec::new(); + let mut total = 0usize; + let mut pass = 0usize; + let mut xfail = 0usize; + let mut fail = 0usize; + let mut both_accept = 0usize; + let mut both_reject = 0usize; + let mut notes: std::collections::BTreeMap = std::collections::BTreeMap::new(); + for round in 0..MUTATED_ROUNDS { + let kind = KINDS + .get(round % KINDS.len()) + .copied() + .unwrap_or(Kind::Null); + let Some((bytes, _)) = generate(kind, &mut r) else { + continue; + }; + // Short values keep the run fast; the long ones are in the valid test. + if bytes.len() > 400 { + continue; + } + let mut m = mutate(&bytes, &mut r); + if r.below(4) == 0 { + m = mutate(&m, &mut r); + } + total = total.saturating_add(1); + let ours = ours_decode(kind, &m); + let theirs = theirs_decode(kind, &m); + match (&ours, &theirs) { + (None, None) => { + both_reject = both_reject.saturating_add(1); + pass = pass.saturating_add(1); + } + (Some(a), Some(b)) => { + both_accept = both_accept.saturating_add(1); + if a == b { + pass = pass.saturating_add(1); + } else if let Some(why) = both_accept_differ(kind, a) { + xfail = xfail.saturating_add(1); + *notes + .entry(format!("both accept, values differ: {why}")) + .or_insert(0) += 1; + } else { + fail = fail.saturating_add(1); + failures.push(format!( + "{kind:?}: both accept {m:02x?} but read {a:?} and {b:?}" + )); + } + } + (Some(a), None) => match we_accept_they_reject(kind, a, &m) { + Some(why) => { + xfail = xfail.saturating_add(1); + *notes + .entry(format!("we accept, `der` rejects: {why}")) + .or_insert(0) += 1; + } + None => { + fail = fail.saturating_add(1); + failures.push(format!( + "{kind:?}: we accept {m:02x?} as {a:?}, `der` rejects it" + )); + } + }, + (None, Some(b)) => match we_reject_they_accept(kind, b, &m) { + Some(why) => { + xfail = xfail.saturating_add(1); + *notes + .entry(format!("we reject, `der` accepts: {why}")) + .or_insert(0) += 1; + } + None => { + fail = fail.saturating_add(1); + failures.push(format!( + "{kind:?}: we reject {m:02x?}, `der` accepts it as {b:?}" + )); + } + }, + } + } + println!( + "clazel-conformance: suite=\"der 0.8.2 differential, mutated bytes\" pass={pass} xfail={xfail} fail={fail} total={total}" + ); + println!( + "both accept {both_accept} (same value), both reject {both_reject}; explained differences:" + ); + for (why, n) in ¬es { + println!(" {n:6} {why}"); + } + assert!( + both_accept > MUTATED_ROUNDS / 60 && both_reject > MUTATED_ROUNDS / 60, + "the mutations did not exercise both outcomes" + ); + assert!( + failures.is_empty(), + "{}", + failures + .iter() + .take(15) + .cloned() + .collect::>() + .join("\n") + ); +} + +/// Silence the unused-constant warning for the types the table above leaves +/// to `x509`: they are read by the decoder and covered by the proofs. +#[test] +fn tag_numbers_are_the_universal_ones() { + assert_eq!( + [ + BOOLEAN, + INTEGER, + BIT_STRING, + OCTET_STRING, + NULL, + OBJECT_IDENTIFIER, + UTF8_STRING, + PRINTABLE_STRING, + IA5_STRING, + UTC_TIME, + GENERALIZED_TIME + ], + [1, 2, 3, 4, 5, 6, 12, 19, 22, 23, 24] + ); + // GeneralizedTime is read as octets like the other strings. + let t = b"20240101000000Z"; + let enc = encode_octets(GENERALIZED_TIME, t); + let got = read_generalized_time(&enc, 0, enc.len()); + assert_eq!(got, Some((2, enc.len()))); + assert_eq!(DerTag::GeneralizedTime.number().value(), GENERALIZED_TIME); +} + +/// The example in the crate's README. +#[test] +fn readme_example() { + use clazel_der::decode::expect_sequence; + use clazel_der::encode::encode_sequence; + let mut body = encode_oid(&[1, 2, 840, 113_549]); + body.extend_from_slice(&[0x02, 0x01, 0x05]); + let bytes = encode_sequence(&body); + let parsed = (|| { + let (start, end) = expect_sequence(&bytes, 0, bytes.len())?; + let (arcs, next) = read_oid(&bytes, start, end)?; + let (negative, ms, me, next) = read_integer(&bytes, next, end)?; + Some(( + arcs, + negative, + bytes.get(ms..me)? == [5], + expect_end(next, end), + )) + })(); + assert_eq!(parsed, Some((vec![1, 2, 840, 113_549], false, true, true))); + assert!(read_tlv(&[0x04, 0x81, 0x26], 0, 3).is_none()); +} diff --git a/docs/DEPENDENCIES.md b/docs/DEPENDENCIES.md index 96d84ca..fb2c0fc 100644 --- a/docs/DEPENDENCIES.md +++ b/docs/DEPENDENCIES.md @@ -88,6 +88,9 @@ Changing a row is a reviewable act. Adding one means saying why below. | simple-dns | 0.12.0 | test | A DNS message parser and writer (balliegojr/simple-dns): the reference engine in `clazel-dns`'s differential test (`tests/conformance_dns.rs`), which writes random messages with it (with and without name compression), reads them with `clazel-dns` and compares, and compares what both read from the same mutated octets. Chosen as the full-message parser with the fewest dependencies that also writes responses: `dnsparse` has none but reads only the header, questions and answers (and uses `unsafe`), and `dns-parser` has two (`byteorder`, `quick-error`) and writes only queries. Its only dependency is `bitflags`. | | httparse | 1.10.1 | test | The HTTP/1.x message-head parser of hyper (Sean McArthur's `httparse`): the reference engine in `clazel-http`'s differential test (`tests/conformance_http.rs`), which compares the status code, every field and the head length it reads from random and mutated responses with ours. It parses only the head, so the framing (`Content-Length`, chunked) is checked against expectations the test builds by construction. Used without default features (`std` adds nothing to it); no dependencies, and its build script uses none. | | bitflags | 2.13.2 | test | Bit-flag macro, required by `simple-dns`. No dependencies. | +| der | 0.8.2 | test | RustCrypto's DER implementation (used by `rsa`, `x509-cert` and `pkcs8`): the reference engine in `clazel-der`'s differential test (`tests/conformance_der.rs`). Used with default features off and only `alloc` and `oid`, so its only dependencies are `const-oid` and `zeroize` (the smallest DER crate that parses and writes every type item 3.5a covers; no other candidate was needed). Pinned to 0.8.2, the latest stable release. | +| const-oid | 0.10.2 | test | `der`'s OBJECT IDENTIFIER type (the `oid` feature). No dependencies with default features off. | +| zeroize | 1.9.1 | test | Zeroing of memory on drop, required by `der`'s `alloc` feature. No dependencies as built. | | serde_json | 1.0.154 | tooling | `xtask` parses `cargo metadata` output. | | serde_core | 1.0.229 | test | Required by `serde_json`. | | itoa | 1.0.18 | test | Integer formatting, required by `serde_json`. |