M5b plan: task 04, PGP private key blocks too
The private-key rule also refuses a line that starts `-----BEGIN` and ends `PRIVATE KEY BLOCK-----`, OpenPGP's armor header (RFC 9580 section 6.2.1; RFC 4880 section 6.2). The two endings are a given table, PRIVATE_KEY_ENDS; the skeleton's private_key comment names it. A PGP private key block joins the attack corpus, a PGP public key block and a signature the code-work corpus, and the edge test gains an indented PGP header (refused), a PGP public header and a PGP header without its closing dashes (both pass). Checks: the reference passes the gate on titan, 997 tests (unchanged: the cases are inside existing tests), /tmp unchanged around the run; the task's edits applied to a fresh ref-03 copy are byte-identical to the reference; 8 private-key mutations, 4 of them new for PGP, all caught; tag and private-key-header greps over the task and given files are empty; given files pass rustfmt and the 500-line limit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ character is refused by `control`.
|
|||||||
| 7 | `secret-sk` | `sk-` keys | `what looks like an API key` |
|
| 7 | `secret-sk` | `sk-` keys | `what looks like an API key` |
|
||||||
| 8 | `secret-slack` | `xoxb-`, `xoxa-`, `xoxp-`, `xoxr-`, `xoxs-` tokens | `what looks like a Slack token` |
|
| 8 | `secret-slack` | `xoxb-`, `xoxa-`, `xoxp-`, `xoxr-`, `xoxs-` tokens | `what looks like a Slack token` |
|
||||||
| 9 | `secret-aws` | `AKIA` or `ASIA` access key ids | `what looks like an AWS access key` |
|
| 9 | `secret-aws` | `AKIA` or `ASIA` access key ids | `what looks like an AWS access key` |
|
||||||
| 10 | `secret-private-key` | a line that starts a private-key block | `a private key` |
|
| 10 | `secret-private-key` | a line that starts a private-key block: PEM, OpenSSH or OpenPGP | `a private key` |
|
||||||
| 11 | `tag` | any listed tag, matched exactly as `neutralise` matches it | `chat-template markup` |
|
| 11 | `tag` | any listed tag, matched exactly as `neutralise` matches it | `chat-template markup` |
|
||||||
|
|
||||||
The table, the sentences and every list of characters and prefixes are **given, filled in**, in the
|
The table, the sentences and every list of characters and prefixes are **given, filled in**, in the
|
||||||
@@ -89,16 +89,18 @@ the new file. That is expected: do not touch `crates/memoryd`.
|
|||||||
and one `ghs_` form, `secret.mixed` is true: the run must also hold an ASCII upper-case letter
|
and one `ghs_` form, `secret.mixed` is true: the run must also hold an ASCII upper-case letter
|
||||||
and an ASCII digit, because names like `sk-folding-cube-spinner` start the same way. `find_secret`
|
and an ASCII digit, because names like `sk-folding-cube-spinner` start the same way. `find_secret`
|
||||||
(given) walks the prefixes and calls your `starts_word` and `key_run`.
|
(given) walks the prefixes and calls your `starts_word` and `key_run`.
|
||||||
- **A private key**: a line that, white space aside, starts `-----BEGIN` and ends
|
- **A private key**: a line that, white space aside, starts `-----BEGIN` and ends with one of
|
||||||
`PRIVATE KEY-----`. A public key or a certificate ends differently and passes; so does the header
|
`PRIVATE_KEY_ENDS` (given): `PRIVATE KEY-----` (PEM and OpenSSH) or `PRIVATE KEY BLOCK-----`
|
||||||
in the middle of a sentence.
|
(OpenPGP's armor header, RFC 9580 section 6.2.1). A public key, a PGP public key block, a
|
||||||
|
signature or a certificate ends differently and passes; so does a header in the middle of a
|
||||||
|
sentence.
|
||||||
- **The tag rule** is task 03's matching, reused: `next_tag` in `tags.rs` finds the first listed
|
- **The tag rule** is task 03's matching, reused: `next_tag` in `tags.rs` finds the first listed
|
||||||
tag exactly as `neutralise` does. Edit E2 lets `refuse.rs` call it. This is narrower than M5a's
|
tag exactly as `neutralise` does. Edit E2 lets `refuse.rs` call it. This is narrower than M5a's
|
||||||
markup rule, which matched pieces of tags in any case: an upper-case tag is not the model's token
|
markup rule, which matched pieces of tags in any case: an upper-case tag is not the model's token
|
||||||
(measured in task 03), and a tag split by a zero-width character is refused by `invisible`.
|
(measured in task 03), and a tag split by a zero-width character is refused by `invisible`.
|
||||||
- **Given code you must not change**: the `RULES` table (its closures turn into `fn` pointers), the
|
- **Given code you must not change**: the `RULES` table (its closures turn into `fn` pointers), the
|
||||||
`Secret` tables, `find_secret` (its `if let … &&` chain is Rust 2024; it compiles on this
|
`Secret` tables, `PRIVATE_KEY_ENDS`, `find_secret` (its `if let … &&` chain is Rust 2024; it
|
||||||
toolchain), `invisible`, `refuse` and the key-character functions.
|
compiles on this toolchain), `invisible`, `refuse` and the key-character functions.
|
||||||
- The calls you need, all in the standard library:
|
- The calls you need, all in the standard library:
|
||||||
|
|
||||||
| Call | Signature, and what it gives |
|
| Call | Signature, and what it gives |
|
||||||
@@ -117,7 +119,7 @@ the new file. That is expected: do not touch `crates/memoryd`.
|
|||||||
| `str::split_at_checked` | `fn split_at_checked(&self, mid: usize) -> Option<(&str, &str)>` |
|
| `str::split_at_checked` | `fn split_at_checked(&self, mid: usize) -> Option<(&str, &str)>` |
|
||||||
| `str::rsplit`, `str::split`, `Iterator::next` | `before.rsplit('\n').next()`: the last line piece; `from.split('\n').next()`: the first |
|
| `str::rsplit`, `str::split`, `Iterator::next` | `before.rsplit('\n').next()`: the last line piece; `from.split('\n').next()`: the first |
|
||||||
| `Option::unwrap_or_default` | `Option<&str>` to `&str`, `""` for `None` (not `unwrap`: library code never panics) |
|
| `Option::unwrap_or_default` | `Option<&str>` to `&str`, `""` for `None` (not `unwrap`: library code never panics) |
|
||||||
| `str::trim`, `str::trim_end`, `str::is_empty`, `str::ends_with` | white space off both ends, off the end; `s.ends_with("PRIVATE KEY-----")` |
|
| `str::trim`, `str::trim_end`, `str::is_empty`, `str::ends_with` | white space off both ends, off the end; `line.ends_with(end)` |
|
||||||
| `next_tag` (in `tags.rs`) | `fn next_tag(text: &str) -> Option<(&str, &'static Tag, &str)>`: before, tag, after |
|
| `next_tag` (in `tags.rs`) | `fn next_tag(text: &str) -> Option<(&str, &'static Tag, &str)>`: before, tag, after |
|
||||||
|
|
||||||
## The edits, exactly
|
## The edits, exactly
|
||||||
@@ -182,9 +184,9 @@ Nothing else in `tags.rs` changes: not its comment, not its body.
|
|||||||
```
|
```
|
||||||
|
|
||||||
The command of step 2 now compiles, with warnings about unused variables, an unused import,
|
The command of step 2 now compiles, with warnings about unused variables, an unused import,
|
||||||
`PRIVATE_USE` and the fields of `Secret`: they are the unfilled functions, and go as you fill
|
`PRIVATE_USE`, `PRIVATE_KEY_ENDS` and the fields of `Secret`: they are the unfilled functions,
|
||||||
them. In `sanitize_refuse` 14 tests fail, in `sanitize_refuse_corpus` 2; every failure is
|
and go as you fill them. In `sanitize_refuse` 14 tests fail, in `sanitize_refuse_corpus` 2;
|
||||||
`not yet implemented`.
|
every failure is `not yet implemented`.
|
||||||
- [ ] **4. Fill the eight functions, one at a time, in this order**, each from its comment, and
|
- [ ] **4. Fill the eight functions, one at a time, in this order**, each from its comment, and
|
||||||
run `cargo check -p bxcore` after each: `control`, `first_char`, `private_use`, `noncharacter`,
|
run `cargo check -p bxcore` after each: `control`, `first_char`, `private_use`, `noncharacter`,
|
||||||
`listed_tag`, `starts_word`, `key_run`, `private_key`. Change nothing else in `refuse.rs`.
|
`listed_tag`, `starts_word`, `key_run`, `private_key`. Change nothing else in `refuse.rs`.
|
||||||
|
|||||||
@@ -315,14 +315,18 @@ fn key_run(after: &str, secret: &Secret) -> bool {
|
|||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How a private-key block's first line ends: RFC 7468's labels and OpenSSH's (`… PRIVATE KEY`),
|
||||||
|
/// and OpenPGP's armor header (`PGP PRIVATE KEY BLOCK`, RFC 9580 section 6.2.1).
|
||||||
|
const PRIVATE_KEY_ENDS: &[&str] = &["PRIVATE KEY-----", "PRIVATE KEY BLOCK-----"];
|
||||||
|
|
||||||
/// The byte offset of the first line whose text, white space aside, starts `-----BEGIN` and
|
/// The byte offset of the first line whose text, white space aside, starts `-----BEGIN` and
|
||||||
/// ends `PRIVATE KEY-----` (RFC 7468's private-key labels, and OpenSSH's).
|
/// ends with one of `PRIVATE_KEY_ENDS`.
|
||||||
fn private_key(text: &str) -> Option<usize> {
|
fn private_key(text: &str) -> Option<usize> {
|
||||||
// For each `(at, _)` in `text.match_indices("-----BEGIN")`: `text.split_at_checked(at)?`
|
// For each `(at, _)` in `text.match_indices("-----BEGIN")`: `text.split_at_checked(at)?`
|
||||||
// gives `(before, from)`; the line up to `at` is `before.rsplit('\n').next()`, and the line
|
// gives `(before, from)`; the line up to `at` is `before.rsplit('\n').next()`, and the line
|
||||||
// from `at` is `from.split('\n').next()`, each `.unwrap_or_default()`. Return `Some(at)` for
|
// from `at` is `from.split('\n').next()`, each `.unwrap_or_default()`. Return `Some(at)` for
|
||||||
// the first `at` where the first line `trim()`s to empty and the second, `trim_end()`ed,
|
// the first `at` where the first line `trim()`s to empty and the second, `trim_end()`ed, ends
|
||||||
// `ends_with("PRIVATE KEY-----")`. `None` if there is none.
|
// with one of `PRIVATE_KEY_ENDS` (`.iter().any(|end| line.ends_with(end))`). Else `None`.
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -267,6 +267,10 @@ fn a_private_key_header_must_be_a_line_of_its_own() {
|
|||||||
("after a line", format!("key:\n{header}\nMII")),
|
("after a line", format!("key:\n{header}\nMII")),
|
||||||
("indented", format!("key: |\n \t {header}\n")),
|
("indented", format!("key: |\n \t {header}\n")),
|
||||||
("with trailing spaces", format!("{header} \nMII")),
|
("with trailing spaces", format!("{header} \nMII")),
|
||||||
|
(
|
||||||
|
"a PGP private key block, indented",
|
||||||
|
format!(" {} \n", pem("BEGIN PGP PRIVATE KEY BLOCK")),
|
||||||
|
),
|
||||||
] {
|
] {
|
||||||
assert_eq!(rule(&text), Some("secret-private-key"), "{name}");
|
assert_eq!(rule(&text), Some("secret-private-key"), "{name}");
|
||||||
}
|
}
|
||||||
@@ -278,6 +282,11 @@ fn a_private_key_header_must_be_a_line_of_its_own() {
|
|||||||
"-----BEGIN RSA PRIVATE KEY".to_string(),
|
"-----BEGIN RSA PRIVATE KEY".to_string(),
|
||||||
),
|
),
|
||||||
("a public key", pem("BEGIN RSA PUBLIC KEY")),
|
("a public key", pem("BEGIN RSA PUBLIC KEY")),
|
||||||
|
("a PGP public key block", pem("BEGIN PGP PUBLIC KEY BLOCK")),
|
||||||
|
(
|
||||||
|
"a PGP block without the closing dashes",
|
||||||
|
"-----BEGIN PGP PRIVATE KEY BLOCK".to_string(),
|
||||||
|
),
|
||||||
("an end line", pem("END RSA PRIVATE KEY")),
|
("an end line", pem("END RSA PRIVATE KEY")),
|
||||||
] {
|
] {
|
||||||
assert_eq!(rule(&text), None, "{name}");
|
assert_eq!(rule(&text), None, "{name}");
|
||||||
|
|||||||
@@ -285,6 +285,17 @@ fn attacks() -> Vec<(&'static str, String, &'static str)> {
|
|||||||
),
|
),
|
||||||
"secret-private-key",
|
"secret-private-key",
|
||||||
),
|
),
|
||||||
|
// OpenPGP's armor header, RFC 9580 section 6.2.1.
|
||||||
|
(
|
||||||
|
"a PGP private key block",
|
||||||
|
format!(
|
||||||
|
"{}\n\nlQOYBF{}\n{}",
|
||||||
|
pem("BEGIN PGP PRIVATE KEY BLOCK"),
|
||||||
|
mixed(40),
|
||||||
|
pem("END PGP PRIVATE KEY BLOCK")
|
||||||
|
),
|
||||||
|
"secret-private-key",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,6 +391,17 @@ fn code_work() -> Vec<(&'static str, String)> {
|
|||||||
pem("END CERTIFICATE")
|
pem("END CERTIFICATE")
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"a PGP public key block and a signature",
|
||||||
|
format!(
|
||||||
|
"{}\n\nmQINBF{}\n{}\n{}\niQIzBA\n{}",
|
||||||
|
pem("BEGIN PGP PUBLIC KEY BLOCK"),
|
||||||
|
mixed(40),
|
||||||
|
pem("END PGP PUBLIC KEY BLOCK"),
|
||||||
|
pem("BEGIN PGP SIGNATURE"),
|
||||||
|
pem("END PGP SIGNATURE")
|
||||||
|
),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"the private-key header named inside a line",
|
"the private-key header named inside a line",
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
Reference in New Issue
Block a user