De-jure standardization - maybe no
We can rule out that it is part of POSIX. POSIX tries to avoid requiring either the full ASCII set, or the ASCII encodings (numeric values), on which this mapping is based. For example, the ETX character you mention is not required by POSIX. Nor does POSIX mention anything about Control-C / ETX or Control-Z / SUB as defaults when discussing the characters used for INTR and SUSP, for example.
As pointed out by others, the Control key behaviour is not inherently part of the definition of a character set / character encoding. It looks like the Control key mapping was not specified as part of ASCII. Nor does it seem to be part of the series of ANSI terminal standards.
De-facto standardization - the VT100?
I think you can partly explain the expectation of this behaviour in terms of the VT100 / VT102 having become the de-facto standard, though I expect the behaviour pre-dates it. See "What protocol/standard is used by terminals?"
See VT102 User Guide, "Transmitted Characters" -> "Function Keys" -> "Control Character Keys".
Figure 4-3 shows the keys that generate control characters. You can generate control characters in two ways.
- Hold down CTRL and press any unshaded key in Figure 4-3.
- Press any shaded key in Figure 4-3 without using CTRL. These dedicated keys generate control characters without the use of CTRL.
Table 4-2 lists the control characters generated by the keyboard. Different computer systems may use each control character differently.
NOTE: The VT102 generates some control characters differently than previous DIGITAL terminals. Table 4-3 lists the changes.
I found this last note particularly interesting. The VT102 uses Control-space for NUL, whereas "previous terminals" from Digital used Control-@. It also changes the escapes for the last two C0 controls, RS and US. (I wonder how this fits into the pattern of flipping bit 7). The VT100 also uses Control-space, so I assume "previous terminals" refers to the VT52 family.
Linux kernel
This is not replicated in different hardware drivers e.g. PS/2 keyboard v.s. USB keyboard. Instead it is handled in the VT layer. See vt/keyboard.c
. The state of the keyboard modifiers, including Control, is maintained in shift_state
. The shift state is then used to select a keymap.
param.shift = shift_final = (shift_state | kbd->slockstate) ^ kbd->lockstate;
param.ledstate = kbd->ledflagstate;
key_map = key_maps[shift_final];
https://elixir.bootlin.com/linux/v4.16.8/source/drivers/tty/vt/keyboard.c#L1393
So for more information I think you would have to look into keymaps as used by the VT layer. I assume the key map for Control is set up to produce the pairing you describe.
The loadkeys man page also mentions the kernel default key map. This has been moved since the man page was written; it is now located at drivers/tty/vt/defkeymap.c_shipped. To read these tables, you must know the Linux keycodes used to index it. They are based on a QWERTY keyboard, so the letters are neither alphabetical nor contiguous. See include/uapi/linux/input-event-codes.h. Or better, this table which shows both keycodes and the default Control mapping.