By the computer in the keyboard. It always was on the PC.
The keyboard is supposed to send properly debounced MAKE/BREAK codes. It also handles autorepeat and knows that some keys are shift keys and lock keys.
It even handles the consequences of new developments and backwards compatibility: the extra ctrl and alt keys couldn't get new keycodes because then existing programs wouldn't recognize them. The solution was to send a special code before MAKE/BREAK of right ctrl/alt.
And it gets worse: there wasn't a separate set of keys for arrows/PgUp/PgDn/Home/End/Ins/Del at first. All we had was the numeric keypad. Whether you got numbers or arrows depended on the xor of num lock and the shift key. So the arrow keys actually share their keycode with the numbers on the numeric keypad! Again the solution involved a special code being sent: before an entirely fake MAKE for one of the shift keys followed by the keycode for one of the numeric keypad keys (MAKE or BREAK), followed by the special code and a fake BREAK for the shift key. And of course, whether you get the synthesized shift key events depends on whether you already have a shift key pressed and on the Num lock status. You can get them for either of the keys sharing the keycode.
Entirely new keys were a lot easier (F11, F12): they just got a new keycode.
On old Windows versions, you could get even more complicated backwards compatibility fun.
The 8086/8088 only had a 1MB address space that wrapped around: if you used a high enough segment value with a high enough offset, you would actually refer to the beginning of memory.
The 80286 had a much larger address space: a whole 16MB! But when running in compatibility mode (where you could only refer to 1MB), the addresses no longer wrapped around. The result was that you actually had an address space of 1MB+64KB (minus 16 bytes). IBM wanted better compatibility than that so they found an unused AND gate in a TTL chip lying around somewhere and put the A20 signal from the CPU through that before it reached the bus. You could then switch A20 off at will, thus getting better compatibility. So how do you control that gate? Well, the microcontroller on the motherboard that talks to the keyboard microcontroller has an extra pin that isn't used for anything. Let's use that pin! So you switch the A20 gate on/off by sending commands to the keyboard controller.
Fast forward some years. We now have a decentish version of Windows that is basically running on top of DOS. It has extensive compatibility with DOS in many ways: not only can it run (and multitask) DOS programs inside it, it can also use DOS device drivers and TSRs loaded before Windows. It does this by merrily switching back and forth between so many different CPU modes that it makes you dizzy. It needs to run with the A20 on, otherwise you would get a really funny address space where every other megabyte was inaccessible, which would be both clumsy and wasteful. And device drivers and TSRs loaded into the 64KB right above the first megabyte would also need A20 on to run. On the other hand, there might be other real mode code in the system that needs A20 to be off! So Windows could switch it on and off while running. It doesn't quite involve the keyboard but it is close... and sometimes a bad keyboard (with bad code in its microcontroller) would distract the microcontroller on the motherboard so much that the A20 switching was slow or even faulty. In that case you could fix your machine by switching keyboard!