Appendix: Writing Software for KLE

This chapter is informative, except the Rules of Software Requirements, which are normative for software. The examples follow Notation and Conventions, under which a vector register loaded from memory holds the lowest-addressed byte in its least significant bits, and assume RV64, where a 64-bit operand is one GPR; on RV32 it is a GPR pair (Rule GR2).

Software Requirements

The architecture does not enforce these Rules: software, privileged or not, that breaks them may corrupt cryptographic state, make it unavailable, or reveal a CC to peer or less privileged software.

Definition: Interposed software.

Interposed software is code that runs while the live KLE state of the hart may hold, for other software, a halted instruction or an open management operation: every exception and interrupt handler, in any mode, including those of the interrupted code, and any other code sharing that KLE state, such as another thread of the same process.

Isolation and the KLIOBUF

SR1

Software must keep its lockers from unauthorized parties. Before giving control to a less privileged mode, it must clear every locker that holds its CC or that it has placed in a Configuration State, completing any export first (Rule SR9), and, if the KLIOBUF holds its data, write to kliobuflen the value that the less privileged mode had saved (Context Save Order), which zeroes the buffer below that length, or 0, which makes the buffer inaccessible (kliobuflen).

SR2

Software should zero kliobuflen once it no longer needs the KLIOBUF contents, which context-switch code must otherwise save and restore (Context Save Order, Context Restore Order); zeroing kliobuftop instead keeps them accessible.

Requirements on klstart

klstart is one per hart, and nothing resets it between programs: an instruction that honors it starts at the value it finds. Without these Rules, interposed software would skip a prefix of its own operand and the interrupted instruction would resume elsewhere, and a KLIOBUF transfer would use the wrong offset, with no exception.

SR3

Interposed software must save klstart (kl.getks or a CSR read) before any other KLE instruction, write 0 to it before its first instruction that honors it, and restore it before returning to the interrupted software. A handler that skips the trapping instruction applies the retirement effects to klstart instead (Rule SR10).

SR4

Software should not write 0 to klstart to restart a kl.exec or kl.derive that has committed per-block state effects.

SR5

Software that transfers an image through the KLIOBUF must save klstart after each kl.mvin KLIOBUF or kl.mvout KLIOBUF, write 0 to it before the kl.input or kl.output that next fills or drains the KLIOBUF, and restore it before the next kl.mvin KLIOBUF or kl.mvout KLIOBUF.

Requirements on Management Operations

The hardware enforces neither Rule SR6 nor SR7: an instruction acts on whatever locker klnamedlocker names, and a kl.mgmt that names its locker explicitly acts on it and overwrites klnamedlocker, whatever operation is open (Rules Governing the Named Locker).

SR6

Before kl.load, kl.store, kl.mvin, kl.mvout, an instruction naming KLNL (Rule GR4), or a kl.mgmt that names its locker explicitly, interposed software must, if klnamedlocker does not hold the intended value, save it, write that value, and restore the saved one before returning to the code that set it. The intended value is the number of the target locker, 32 for a transfer through the KLIOBUF, and either for a kl.mgmt that names its locker explicitly. Any software that completes or abandons a management operation must then write 32 to klnamedlocker, or restore the value it saved.

SR7

While a locker is in State kl_cfg_exporting or kl_cfg_importing, whether or not klnamedlocker names it, software, interposed or not, must save it by a nested export before it issues a kl.mgmt on another locker, and restore it before returning to the code that opened its operation, or terminate that code; it may instead clear a locker in kl_cfg_importing, whose import then restarts (Management of Cryptographic Contexts). Until then, software must write to klnamedlocker only that locker’s number or 32: context-switch code finds through it the locker that owns the authentication registers, of which the hart has one set (Ownership of Authentication Registers, The Cryptographic Locker File and Authentication Information).

SR8

Software should complete or discard a management operation before it performs cryptographic operations or manages other lockers; code that must interleave them, such as threads of one process, needs coordination, such as a mutex or lightweight threads with KLE context-switching support. Software that writes 32 to klnamedlocker while its own import or export is open, for instance for a transfer through the KLIOBUF (kl.mvin and kl.mvout), must have no other locker in State kl_cfg_importing or kl_cfg_exporting, even one that a nested export has saved: context-switch code then finds the open operation by its State (Context Save Order).

SR9

At a context switch, system software may clear a locker under a provisioning or an import instead of saving it, which restarts the operation; it must, however, be able to save such a locker, and must not clear it unconditionally at every context switch, which may keep the operation from ever completing. Unless it terminates the owner, software must not clear a locker under an export, base or nested, before completing that export: the clear destroys the CC.

Requirements on Exception Handlers

SR10

A handler that neither resolves nor forwards, even to an in-process handler, an exception for which Error State that a handler that neither resolves nor forwards an exception sets gives an Error State, and does not terminate the process, must:

  1. set the locker on which the exception was raised, for KLNL the one that klnamedlocker names (Rule GR21), to the State that the same table gives, with a kl.setste (Rule GR26), even if the locker is Unconfigured; and

  2. advance xepc past the trapping instruction, which would otherwise trap again, and, for a usage instruction, keep the output prefix that klstart and vstart record, zero the rest of the output window, and apply the retirement effects on klstart and vstart (Rules GR14 and GR7).

SR11

Software must not rely on whether an uninterruptible instruction (Rule GR32) has retired when a handler runs, and must interact with it only as Rule GR35 prescribes.

Requirements on Localities

SR12

Software must not unconfigure a Locality Secret while a locker bound to it may still be exported: the export of such a locker in a Valid State is refused (Generation of an SCC from a Fully Configured Locker) until the Locality Secret is configured again.

SR13

Before running software not entrusted with the CCs of the loaded virtual machine, a hypervisor must write zeros(128), or a value that no virtual machine uses, to hkllocality and hklvirtbootscrt (hkllocality, hklvirtbootscrt).

Requirements on Transfers, Keys and Outputs

SR14

Software must bound a sequence of kl.mvin or kl.mvout by the image size that kl.size reads before it, not by klstart (Rule GR13.2), and check the State after it (Appendix: Writing Software for KLE, NOTE after Rule GR41).

SR15

Software must rely on an exported image only if the State read after the last instruction of the export, or in the MDH that kl.getmd* returned when the image is the MDH alone, is not Unconfigured (Rule GR13.2). After a kl.mgmt that opens an export from a Valid State, software must continue only if the State is kl_cfg_exporting; a Valid State means a refusal (Generation of an SCC from a Fully Configured Locker).

SR16

M-mode must not change the CSK (Architectural Requirements on the Context Sealing Key) of a hart until every export open on it, base or nested, has completed and its image has been saved.

SR17

A system that intends to protect a System Key (System Keys) must associate at least a UsagePolicy with its SKID in the SKS.

SR18

Software must not use plaintext that an AEAD Machine outputs before the locker reaches Success for that message, and must discard it if the locker does not.

SR19

Software must not rely on a value read from the clear blocks of an SCC in memory (GR9) as authentic until an import of the SCC, or other means, has authenticated it.

Software Guidelines

The guidelines of this section are for privileged software.

Software that performs a context switch or otherwise uses the KLE state while that of a less privileged context is live, saves and restores that state by following Context Save Order and Context Restore Order, so that the less privileged context sees its own values and no data of the more privileged mode; it meets Rule SR1 the same way. Where an item cites a Rule SRn (Software Requirements), that Rule takes precedence.

Duties of Every Exception Handler

Interposed software is bound by Rules SR3, SR6 and SR7.

On entry, before its first KLE instruction, the handler (which reads and writes klstart and klnamedlocker with the getter and setter instructions, CSR Getters and Setters: kl.get*, kl.set*, and kl.set*i):

  1. saves klstart (kl.getks, Rule SR3);

  2. saves klnamedlocker (kl.getnl, Rule SR6);

  3. writes 0 to klstart (kl.setksi 0) before its first instruction that honors it (Rule SR3); and

  4. saves the vector state, and writes 0 to vstart, before its first vector KLE instruction (Rule GR6).

Before each instruction that Rule SR6 lists, it sets klnamedlocker as that Rule requires.

On exit, the handler:

  1. restores klnamedlocker (kl.setnl) to its saved value, or to 32 if it has cleared, instead of saving, the locker that value named (Context Save Order);

  2. restores the vector state, including vstart; and

  3. restores klstart last (kl.setks), exactly as saved (Rules SR3 and SR4).

A handler that skips the trapping instruction applies its retirement effects to klstart and vstart instead of restoring them (Rule SR10).

Reading the State of a Locker

  • kl.getst Xd, KLNL reads the State of the locker that klnamedlocker names; with klnamedlocker = 32 it returns 0 (Rule GR13.2).

  • To inspect lockers by number, software writes each number in turn to klnamedlocker and names KLNL in the following instructions. While an import or export is open, it first saves that locker by a nested export (Rule SR7).

  • kl.getst reads the State, kl.getstx the StateExtension, and kl.getmd the whole MDH.

  • On RV32, kl.getst and kl.getstx write the GPR pair X[d+1] @ X[d], and kl.getmd a quartet (kl.getst and kl.getstx, kl.getmd, kl.getmdl, and kl.getmdv).

  • A locker with State 0 is Unconfigured if its StateExtension is 0, and in an Error State otherwise (State and StateExtension Fields).

  • Before using the output of a usage instruction, software checks, immediately after the instruction, that the locker is in a Valid State.

Management Routines

  • A management routine should take the locker number as a parameter, 32 meaning the locker that klnamedlocker names. It saves klnamedlocker; on entry and at every restart, it writes the number to it, or, for 32, keeps its value and returns an error to its caller if that value is also 32 (Rule GR13.2). It names the locker KLNL throughout, and restores the saved value on exit, even when it abandons the operation, unless the saved value names the locker that a clear has destroyed, in which case it writes 32 (Rules SR6, Code for Locker Configuration and Export Operations).

  • After each management instruction, the routine reads the State of the locker: Unconfigured means it has been cleared, so a provisioning or an import restarts and an export has lost its CC (Rule GR13.2); an Error State means the operation has failed. After an export-opening kl.mgmt, a Valid State means that the export was refused: the routine reports an error and leaves the locker in place. One code path handles resumption and restart.

  • An export sizes its buffer with kl.size, and reads the size again after its opening kl.mgmt.

  • Software does not interleave cryptographic operations, or the management of other lockers, with the steps of a management operation (Rule SR8).

Context Save Order

  1. Save klstart (kl.getks).

  2. Save klnamedlocker (kl.getnl).

  3. Let the managed locker be the locker that the saved klnamedlocker indexes, if it is in [0..31]; otherwise, the locker in State kl_cfg_importing or kl_cfg_exporting, if there is one (Rules SR7 and SR8). These Rules make it the owner of the authentication registers, if a locker owns them (Ownership of Authentication Registers). If there is a managed locker, save it first, with Form C or D kl.mgmt (Rule SR7), choosing the path by its State, read with kl.getst, and transferring through GPRs or vector registers, not the KLIOBUF, which step 4 has yet to save:

    1. Unconfigured: save no locker image;

    2. an Error State: save its MDH by the short Error-State path (Export and Import of a Locker in an Error State);

    3. otherwise, export the locker, by nested export if it is in a Configuration State.

  4. With Zklio:

    1. save kliobuflen and kliobuftop (kl.getbl, kl.getbt);

    2. if kliobuflen is non-zero, save the buffer contents with kl.output, with klnamedlocker = 32, klstart = 0 and kliobuftop = kliobuflen (kl.setnli 32, kl.setksi 0, kl.setbt of the saved length), then write 0 to kliobuflen (kl.setbli 0).

  5. For every locker other than the managed locker, act by its *llockerstatus field that the saving mode owns (mllockerstatus, sllockerstatus, and vsllockerstatus for Dirtiness Tracking), a locker without such a field counting as Dirty: keep the saved image of a Clean or Off locker; otherwise, save the locker by the paths of step 3 if it is not Unconfigured, and erase its saved image if it is. Keep klnamedlocker = 32 between exports.

Each export completes, with its completing kl.mgmt and outcome check, before its locker is cleared (Rule SR9).

An export-opening kl.mgmt that leaves a Valid locker in its Valid State was refused (Rule SR15): the handler saves no image for that locker and reports the error to the code that owns it, and the restore order clears the locker, whose CC is lost. Software that complies with Rule SR12 and configures the Locality Secret CSRs before it exports (Locality Secret CSR Groups) encounters no refused export.

Software does not read or compare IMPQUAL: the unit does, on import. An image restored on a unit whose IMPQUAL differs has its ADS dropped (ADSDropped in kl.getmd), and a halted long-running operation restarts; software that migrates a locker to such a unit may call kl.clearads before the export, so that the operation restarts on the target.

The handler may clear a locker being provisioned or imported instead of saving it (Rule SR9); the interrupted software then restarts the operation (Rule GR13.2), and the handler restores 32 in place of a saved klnamedlocker that named that locker (Context Restore Order).

Context Restore Order

  1. Clear the lockers for which no image is restored below, or all of them with kl.clearall (Rule SR1).

  2. Restore the Locality Secret CSRs that the context owns, if any (Locality Secret CSR Groups), before any import of a locker that they bind.

  3. Restore the lockers other than the managed locker (Context Save Order), with klnamedlocker = 32 between imports.

  4. With Zklio:

    1. restore kliobuflen (kl.setbl), which zeroes the buffer and sets kliobuftop to it;

    2. restore the buffer contents, if saved, with kl.input, with klnamedlocker = 32, klstart = 0 and kliobuftop = kliobuflen;

    3. restore kliobuftop (kl.setbt), after the buffer, since a write of kliobuflen resets it.

  5. If an image was saved for the managed locker, restore it last, with Form C or D kl.mgmt, transferring through GPRs or vector registers, not the KLIOBUF, which step 4 has restored.

  6. Restore the saved value of klnamedlocker, or 32 if the handler has cleared the locker it named instead of saving it.

  7. Restore klstart after every other KLE instruction (kl.setks).

  8. Write the *llockerstatus fields that the restoring mode owns: Clean for a restored locker, Off for a locker whose restoration is deferred, and Initial for a cleared locker. Then write KLS, after those CSRs (KLE Context Status in mstatus and sstatus).

The getter and setter instructions are the recommended way for software to access klnamedlocker, kliobuflen, kliobuftop and klstart (CSR Getters and Setters: kl.get*, kl.set*, and kl.set*i).

Probing the Maximum KLIOBUF Length

As with vsetvl, the getter and setter instructions give the maximum KLIOBUF length: write a large value with kl.setbl, then read it back with kl.getbl. A context restore does not probe: each OS, or other execution environment, probes once and keeps the value, which only an M-mode write to mklmaxiobuflen changes (mklmaxiobuflen). The probe destroys the buffer contents: save them first, or probe only while the buffer is unused.

  kl.getbl  t1      # save the current length
  kl.setbl  a0      # zeroes the buffer, clamps to the maximum
  kl.getbl  t0      # t0 <- length granted
  kl.setbl  t1      # restore the length; the contents stay zero

Handling a KLE Exception

A handler that can neither resolve nor forward a KLE exception for which Error State that a handler that neither resolves nor forwards an exception sets gives an Error State, nor terminate the process, follows Rule SR10.

For KLE fatal-error, missing-CSK and locker-Off, which have no Error State, a handler issues no kl.setste: it resolves the exception, forwards it (for locker-Off, see mllockerstatus, sllockerstatus, and vsllockerstatus for Dirtiness Tracking), or terminates the process.

Code for Locker Configuration and Export Operations

This section gives the instruction sequences for provisioning, import and export. They show the operation and its error detection, not application-specific error handling, and can be used as a starting point for developing libraries or OS APIs.

Registers

GPR t0 holds the number of the locker to be provisioned, imported or exported, or 32 for the locker that klnamedlocker names, and GPR t6 points to the 16-byte aligned memory location of the PI or SCC (Rule GR12). The MDH is loaded and stored with ld and sd, and passed in the GPR pair s5:s4; on RV32 it takes a GPR quartet (Rule GR2).

Selecting the locker

The routines encode no locker number: they save klnamedlocker on entry, select the locker with the preamble select_locker below on entry and at every restart, and every KLE instruction then acts on that locker (Rules GR4 and GR4): kl.load, kl.store, kl.mvin and kl.mvout have no locker operand, and kl.mgmt, kl.getst, kl.getmd and kl.size name KLNL. A clear resets klnamedlocker to 32, after which a KLNL opening performs no operation (Rule GR13.2), whereas an opening on a named locker opens it, even a Valid one. The next State check then reads 0, and the preamble at restart names locker t0 again or, if t0 = 32, branches to no_locker, since the clear destroyed the locker. On exit the routines restore the saved value (Rule SR6), or write 32 if it names a locker that a clear destroyed (Error handling below), so a handler that has saved the interrupted operation (Software Guidelines) can call them, also for a nested export or import of the locker that operation manages.

.macro select_locker              # t0 = p: 0-31, or 32 for the named locker
  li        t1, 32
  bgeu      t0, t1, 1f            # p = 32: keep the named locker
  kl.setnl  t0                    # p in 0-31: name locker p
  j         2f
1:
  kl.getnl  t2
  bgeu      t2, t1, no_locker     # none named, or cleared: error to caller
2:
.endm
Checking the State

After each management instruction, the code reads the State with kl.getst and, if it is 0, the StateExtension with kl.getstx. If a handler has meanwhile cleared the locker or set an Error State, every following kl.mvin, kl.mvout, kl.load, kl.store or kl.mgmt, including a KLNL opening or completion after a clear, performs no operation (Rule GR13.2), and the check detects the change:

  • after a clear, KLNL reads as Unconfigured, with State and StateExtension 0: a provisioning or an import restarts at its preamble, and an export, whose CC is destroyed, branches to handle_errors;

  • after an Error State, or after the completing kl.mgmt, KLNL still names the locker, so the check reads its final State and StateExtension; an Error State abandons the operation.

kl.load and kl.store take Xl = x0, so after a clear they are a kl.input or kl.output with Xl = 0, which performs no operation (Rule GR11). The macro check_state makes this check for provisioning and import; an export branches to handle_errors on State 0 alone.

.macro check_state on_clear       # after a management instruction
  kl.getst  t2, KLNL
  bnez      t2, 1f                # State not 0: no clear, no Error State
  kl.getstx t2, KLNL
  beqz      t2, \on_clear         # Unconfigured: the locker was cleared
  j         handle_errors         # Error State
1:
.endm

Only the check after the last instruction is required; the earlier ones react sooner. The code may bound the restarts and then branch to its error handling, for robustness only: system software must let the operation complete (Rule SR9).

Error handling

The code at handle_errors and no_locker is not shown; no_locker returns an error to the caller, since with t0 = 32 no locker is named, or a clear has destroyed the named one. If the saved value of klnamedlocker names the locker that a clear has destroyed, both write 32 to klnamedlocker instead of restoring it, as a handler that clears a locker instead of saving it restores 32 (Context Restore Order); this is always the case for no_locker reached after a clear with t0 = 32, and for handle_errors reached on an export whose locker has been cleared, if the caller had named that locker. handle_errors writes 0 to klstart, which a kl.mvin or kl.mvout loop leaves at its last transfer point, and restores klnamedlocker as the routines do on exit. If the locker is still in a Configuration State, handle_errors clears it if it is being provisioned or imported and, if it is being exported, completes the export with the saved MDH, which restores the locker.

Provisioning

The provisioning code assumes that the Content immediately follows the MDH in memory. The MDH of the PI carries State = 0; the completing kl.mgmt sets Ready.

Provisioning Code (Zklmv)

# t5 holds the length of the PI's Content (hence, excluding the MDH) in bytes.
# t5 can be zero. The caller constructed the PI and so knows this length.
# t5 is never modified, so the working count and pointer can be re-derived
# from it whenever the operation is restarted.

  ld        s4, 0(t6)             # load MDH[63:0] from memory
  ld        s5, 8(t6)             # load MDH[127:64] from memory
  kl.getnl  s8                    # save the caller's value: the routine may
                                  # run inside another management operation

restart:
  select_locker                   # name the locker: every KLE instruction
                                  # below acts on it; a clear resets it to 32
  kl.mgmt   KLNL, #kl_cfg_provisioning, s5:s4 # start provisioning, load MDH
  check_state restart             # Error State: provisioning ended

  addi      t4, t6, 16            # source pointer: first byte after MDH
  mv        s1, t5                # working byte count, re-derived from t5
                                  # on restart

  vsetvli   s6, zero, e8, m1, ta, ma # s6 = VLMAX
vloop:
  mv        s2, s1
  bleu      s2, s6, 1f
  mv        s2, s6
1:
  vsetvli   s2, s2, e8, m1, ta, ma # AVL <= VLMAX, so vl = AVL, multiple of 16
  vle8.v    v1, (t4)              # load a chunk of the PI
  kl.mvin   v1                    # move it into the locker
  add       t4, t4, s2            # bump the source pointer
  sub       s1, s1, s2            # decrement the bytes still to load
  check_state restart             # *new* Error State
  bnez      s1, vloop

  kl.mgmt   KLNL, #kl_cfg_management_end # complete provisioning
                                  # and possibly reconstruct data
  check_state restart             # catches errors at completion (e.g., SKS)

finished:
  kl.setnl  s8                    # restore the caller's value
# The locker is Ready, or in Failure after a data error at completion. Any
# Error State met above is new, since the PI carried State = 0:
# handle_errors decides the course of action.

Provisioning Code (Zklmem)

  ld        s4, 0(t6)             # load MDH[63:0] from memory
  ld        s5, 8(t6)             # load MDH[127:64] from memory
  kl.getnl  s8                    # save the caller's value

restart:
  select_locker                   # name the locker (a clear resets it to 32)
  kl.mgmt   KLNL, #kl_cfg_provisioning, s5:s4 # start prov. and load MDH
  check_state restart             # Error State?

  # Assume that the Content directly follows the MDH in memory.
  kl.load   16(t6), x0            # load rest of PI into the locker
  check_state restart
  kl.mgmt   KLNL, #kl_cfg_management_end # complete provisioning
                                  # and possibly reconstruct data
  check_state restart             # catches errors at completion (e.g., SKS)
finished:
  kl.setnl  s8                    # restore the caller's value

Export

Export Code (Zklmv)

  kl.getnl   s8                   # save the caller's value
  select_locker                   # name the locker for all that follows
  kl.size    s1, KLNL             # TOTAL size to export
  beqz       s1, handle_errors    # 0 means the locker is unconfigured

  {...}  # here code that allocates the buffer to store the exported data

  kl.getmd   s5:s4, KLNL          # get MDH, including its _State_
  beqz       s4, handle_errors    # all zero: locker cleared since kl.size
  sd         s4, 0(t6)            # store MDH[63:0] to memory
  sd         s5, 8(t6)            # store MDH[127:64] to memory

  addi       t6, t6, 16           # point to next memory region
  li         s3, 16               # s3 = 16 (size of MDH)
  beq        s1, s3, done         # only the MDH to store (e.g., Error State)

  kl.mgmt    KLNL, #kl_cfg_exporting  # start exporting
  kl.getst   t2, KLNL
  beqz       t2, handle_errors    # cleared, or Error State set meanwhile
  li         t3, kl_cfg_provisioning # 56, the lowest Configuration State
  bltu       t2, t3, export_refused # Valid State: export refused (Rule SR15)
  kl.size    t2, KLNL             # size now that the export is open
  bne        t2, s1, handle_errors # ADS changed since the buffer was sized
  addi       s1, s1, -16          # adjust size to remove the MDH
  mv         t4, t6               # destination: first byte after stored MDH
  vsetvli    s6, zero, e8, m1, ta, ma # s6 = VLMAX
store_loop:
  mv         s2, s1
  bleu       s2, s6, 1f
  mv         s2, s6
1:
  vsetvli    s2, s2, e8, m1, ta, ma # AVL <= VLMAX, so vl = AVL, multiple of 16
  kl.mvout   v2                   # move chunk out of the locker,
                                  # advance klstart
  vse8.v     v2, (t4)             # store it
  add        t4, t4, s2           # bump the destination pointer
  sub        s1, s1, s2           # decrement the bytes still to store
  kl.getst   t2, KLNL
  beqz       t2, handle_errors    # cleared (CC lost), or Error State
  bnez       s1, store_loop

finished:
  # Restore _State_ from the saved MDH.
  kl.mgmt    KLNL, #kl_cfg_management_end, s5:s4
  kl.getst   t2, KLNL
  beqz       t2, handle_errors    # cleared, or `kl_stateext_mgmt_auth`
done:
  kl.setnl   s8                   # restore the caller's value

This is an incomplete procedure, like all the others in management. A locker in an Error State is covered: only its MDH is stored. export_refused reports a refused export and leaves the locker in place (Management Routines). A locker under management is exported by the same sequence, but the caller must also save klstart before the opening kl.mgmt, which clears it, and restore it after the re-import (Software Guidelines); this is not shown.

Export Code (Zklmem)

# Up to the size check after the opening kl.mgmt, and from finished on, the
# code is that of the Zklmv version.
  kl.store  0(t6), x0             # store SIV, IMPQUAL, SIV2 and Content
                                  # of the locker (Xl = x0)
  kl.getst  t2, KLNL
  beqz      t2, handle_errors     # cleared (CC lost), or Error State

Import

Import Code (Zklmv)

  ld         s4, 0(t6)            # load MDH[63:0] from memory
  ld         s5, 8(t6)            # load MDH[127:64] from memory
  srli       s7, s4, 18           # bits [27:18] of the saved MDH:
  andi       s7, s7, 0x3ff        # _StateExtension_ @ _State_
  kl.getnl   s8                   # save the caller's value

restart:
  select_locker                   # name the locker (a clear resets it to 32)
  kl.mgmt   KLNL, #kl_cfg_importing, s5:s4    # open locker for import, load MDH
  kl.getst  t2, KLNL
  bnez      t2, 2f                # State not 0: the import is open
  kl.getstx t2, KLNL
  beqz      t2, restart           # Unconfigured: locker cleared, restart
  slli      t2, t2, 6             # Error State: if as saved, the short import
  beq       t2, s7, success       # is complete, with this one kl.mgmt
  j         handle_errors         # new Error State (e.g., invalid Metadata)
2:
  kl.size   s1, KLNL              # size of the image the locker accepts,
                                  # without a dropped ADS
  beqz      s1, restart           # 0: locker cleared since the check
  addi      t4, t6, 16            # first byte after the MDH
  addi      s1, s1, -16           # image size to load, without the MDH

  vsetvli   s6, zero, e8, m1, ta, ma # s6 = VLMAX
vloop:
  mv        s2, s1
  bleu      s2, s6, 1f
  mv        s2, s6
1:
  vsetvli   s2, s2, e8, m1, ta, ma # AVL <= VLMAX, so vl = AVL, multiple of 16
  vle8.v    v2, (t4)              # load a chunk of the SCC
  kl.mvin   v2                    # move it into the locker, advance klstart
  add       t4, t4, s2
  sub       s1, s1, s2
  check_state restart
  bnez      s1, vloop

  kl.mgmt   KLNL, #kl_cfg_management_end, s5:s4 # decrypt and auth SCC
                                  # and restore _State_ from the saved MDH
  check_state restart             # catches "Authentication Failed"

success:
  kl.setnl  s8                    # restore the caller's value

Import Code (Zklmem)

  ld        s4, 0(t6)             # load MDH[63:0] from memory
  ld        s5, 8(t6)             # load MDH[127:64] from memory
  srli      s7, s4, 18            # bits [27:18] of the saved MDH:
  andi      s7, s7, 0x3ff         # _StateExtension_ @ _State_
  kl.getnl  s8                    # save the caller's value

restart:
  select_locker                   # name the locker (a clear resets it to 32)
  kl.mgmt   KLNL, #kl_cfg_importing, s5:s4    # open locker for import, load MDH
  kl.getst  t2, KLNL
  bnez      t2, 2f                # State not 0: the import is open
  kl.getstx t2, KLNL
  beqz      t2, restart           # Unconfigured: locker cleared, restart
  slli      t2, t2, 6             # Error State: if as saved, the short import
  beq       t2, s7, success       # is complete, with this one kl.mgmt
  j         handle_errors         # new Error State (e.g., invalid Metadata)
2:
  addi      t4, t6, 16            # first byte after the MDH

  kl.load   0(t4), x0             # load rest of SCC into the locker
  check_state restart
  kl.mgmt   KLNL, #kl_cfg_management_end, s5:s4 # decrypt and auth locker,
                                  # if necessary complete it
  check_state restart             # catches `kl_stateext_mgmt_auth`

success:
  kl.setnl  s8                    # restore the caller's value

The opening kl.mgmt of provisioning and import receives the MDH in Form D (Provisioning- and Import-Opening kl.mgmt) and checks all of it, even for the image of a locker in an Error State (Export and Import of a Locker in an Error State). The completing kl.mgmt KLNL, #kl_cfg_management_end can still transition the locker to an Error State, at SKS resolution (provisioning) or authentication (import); the kl.getst after it detects this, and also a clear or an Error State set earlier. A data error found by a Machine check on a field of the PI or SCC leaves the locker in Failure instead (Rule GR25); these listings do not test for it.

Behavior of Routines Whose Locker is Cleared by a Handler

This section follows the provisioning, export and import routines above (Provisioning, Export and Import, both the Zklmv and the Zklmem variants) when an exception handler clears their locker in mid-operation instead of saving it.

A handler may do this to a locker under provisioning or import; it may clear a locker under export only if it terminates the owner of the CC (Rule SR9), but an export routine that resumes after such a clear would still behave as described here: it would detect the loss, because its first check reads State 0. An export routine resumes after such a clear only if the more privileged software has a defect. The routine must then assume it has irrecoverably lost state, and terminate.

The handler

The handler saves klnamedlocker on entry. Its clear sets klnamedlocker to 32 (Rule GR21), and on exit it writes 32 back instead of the saved value, since that value names the locker it has cleared (Duties of Every Exception Handler, Context Save Order and Context Restore Order). The routine resumes with klnamedlocker = 32.

The instructions of the routine

With klnamedlocker = 32, the routine’s instructions find no locker (Rule GR13.2):

  • kl.mvin and every kl.mgmt naming KLNL, including the completing one, do nothing;

  • kl.mvout writes zeros to its vector destination, which the export routine may store to memory before its next check;

  • kl.load and kl.store become a kl.input and a kl.output with Xl = x0, which do nothing (Rule GR11);

  • the next check reads State 0: check_state in a provisioning or an import, which also reads StateExtension 0, and kl.getst in an export.

The next step of the routine
  • A provisioning or an import with t0 in 0–31 branches to restart. There select_locker names locker t0 again, the opening kl.mgmt zeroizes and reopens it, and the operation starts over.

  • A provisioning or an import with t0 = 32 branches to restart, where select_locker reads 32 and branches to no_locker. The caller gets an error, because nothing records which locker it meant.

  • An export branches to handle_errors at its first check, because the clear has destroyed the CC; the memory image it has written so far is incomplete and must be discarded.

Why a clear resets klnamedlocker

Suppose a clear left klnamedlocker unchanged. KLNL would then still name the cleared locker:

  • While that locker stays Unconfigured, the instructions of the routine fall through on it, and the next check reads State 0. An export branches to handle_errors, and a provisioning or an import with t0 in 0–31 restarts, as above. With t0 = 32, however, a provisioning or an import would miss the loss: at restart, select_locker would find the number of the cleared locker in klnamedlocker, and the routine would reopen that locker and provision it, or import into it, although the handler meant to free it.

  • Once the locker has been given to another context, the instructions of any of the routines, including an export, would act through KLNL on that context’s locker instead of falling through, and its checks would read that locker’s State.

Resetting klnamedlocker to 32 lets a routine that knows its locker only as the named locker notice a clear. For the same reason, a clear of the named locker is treated as a reconfiguration of it.

Substituting the KLIOBUF for Vector Registers

With Zklio, the substitutions of Rule GR10 combine with kl.input and kl.output into sequences equivalent to the native instructions, as Table 1 shows.

Table 1. Substitution Sequences for Various Instructions
Instruction and Form Native Instruction Substitution Sequence

kl.exec, Form A

kl.exec  Vd, Kn, Vs2

1: kl.input  %offset(Xs), Xl
2: kl.exec   Kn
3: kl.output %offset(Xs), Xl

kl.exec, Form B

kl.exec      Kn, Vs2

1: kl.input  %offset(Xs), Xl
2: kl.exec   Kn

kl.exec, Form C

kl.exec  Vd, Kn

1: kl.exec   Kn
2: kl.output %offset(Xs), Xl

The architecture does not enforce these sequences or their order. kl.setst and kl.mgmt have no substitution: code without vectors uses Form D in place of Form C.

Code for the Machines

The listings of Code for the Machines are RV64 code for the V extension (VLEN ≥ 128) and Zbb; locker K0 is the target, and ECDH and One-Step Key Derivation into an AES-256 Key also uses K1 and K2. Listing Implementing GCM encryption using an ECB CC also needs Zvkg (vghsh.vv). Arguments arrive in a0–a7, s9 and s10; error is the caller’s error handling, not shown, which erases the output buffer of a decryption before it reports (Rule SR18). import_scc n, rs stands for the import code of Import with t0 = n and t6 = rs; it changes no a register, s9 or s10, but it changes vtype. A 16-byte block is processed with vl = 16 and SEW = 8, so KLLEN = 128, as in most examples, where b = 128; a shorter piece of p bytes is processed with vl = p, so KLLEN = 8 × p. Most listings omit the kl.getst that checks, before an output is used, that the locker is still in a Valid State (Reading the State of a Locker): a usage instruction on a locker that a handler has cleared or invalidated performs no operation and writes zeros.

Electronic Codebook Mode

The following listing encrypts data with an ECB CC.

Simple Example: Encryption with no Authentication
# a0 = SCC, a1 = plaintext, a2 = ciphertext buffer, a3 = length in bytes,
# a multiple of 16

  import_scc 0, a0                # import the SCC into locker K0
  kl.getst  t1, K0
  beqz      t1, error             # State 0: the import failed

  kl.setst  K0, #kl_state_encrypt # configure the locker to encrypt
  kl.getst  t1, K0
  li        t2, kl_state_encrypt
  bne       t1, t2, error

  vsetivli  zero, 16, e8, m1, ta, ma # one block: KLLEN = 128
  beqz      a3, 2f
1:
  vle8.v    v0, (a1)              # one block of plaintext
  kl.exec   v0, K0, v0            # encrypt it
  vse8.v    v0, (a2)
  addi      a1, a1, 16
  addi      a2, a2, 16
  addi      a3, a3, -16
  bnez      a3, 1b
2:

In Encrypt and Decrypt a Form A kl.exec processes every block of its operand (Electronic Codebook Mode), so one instruction can encrypt several blocks inside the KLE unit. The following listing encrypts eight blocks per kl.exec, with KLLEN = 1024, and the last group with a smaller multiple of 128 bits. With LMUL = 8 and VLEN ≥ 128, a register group holds at least 128 bytes. An interrupt may halt such a kl.exec after some blocks; on return, the same instruction resumes from klstart (klstart), so the loop needs no code for it.

ECB Encryption, Eight Blocks per Instruction
# a0 = SCC, a1 = plaintext, a2 = ciphertext buffer, a3 = length in bytes,
# a multiple of 16

  import_scc 0, a0                # import the SCC into locker K0
  kl.setst  K0, #kl_state_encrypt # configure the locker to encrypt
  kl.getst  t1, K0
  li        t2, kl_state_encrypt
  bne       t1, t2, error         # the import failed

  li        t3, 128               # eight blocks
  beqz      a3, 2f
1:
  minu      t4, a3, t3            # this group: 16 to 128 bytes
  vsetvli   zero, t4, e8, m8, ta, ma # KLLEN = 8 * t4
  vle8.v    v8, (a1)              # up to eight blocks of plaintext
  kl.exec   v8, K0, v8            # encrypt them all
  vse8.v    v8, (a2)
  add       a1, a1, t4
  add       a2, a2, t4
  sub       a3, a3, t4
  bnez      a3, 1b
2:

Chaining/Feedback Modes (CBC, CFB, and OFB)

The kl.setst into Encrypt or Decrypt of a CBC or CFB CC takes the IV as a Form C operand (Chaining/Feedback Modes (CBC, CFB, and OFB)), and the CC keeps the chaining value across kl.exec instructions, so the loop needs no code for it. An interrupt may halt a kl.exec after some blocks, with the chaining value of the last completed block; on return, the same instruction resumes from klstart (klstart). The following listing encrypts with a CBC CC, one block per kl.exec.

CBC Encryption
# a0 = SCC, a1 = plaintext, a2 = ciphertext buffer, a3 = number of blocks,
# a5 = the 16-byte IV

  import_scc 0, a0                # import the SCC into locker K0
  vsetivli  zero, 16, e8, m1, ta, ma # one block: KLLEN = 128
  vle8.v    v1, (a5)
  kl.setst  K0, #kl_state_encrypt, v1  # IV <- v1
  kl.getst  t1, K0
  li        t2, kl_state_encrypt
  bne       t1, t2, error         # the import failed

  beqz      a3, 2f
1:
  vle8.v    v0, (a1)
  kl.exec   v0, K0, v0            # chained to the previous block
  vse8.v    v0, (a2)
  addi      a1, a1, 16
  addi      a2, a2, 16
  addi      a3, a3, -1
  bnez      a3, 1b
2:

With VLEN = 128 and LMUL = 8, a register group holds eight blocks, and one kl.exec chains them in order inside the KLE unit. The following listing takes the number of blocks as a parameter; for the last group, of fewer than eight blocks, it reduces vl.

CBC Encryption, Eight Blocks per Instruction
# a0 = SCC, a1 = plaintext, a2 = ciphertext buffer, a3 = number of blocks,
# a5 = the 16-byte IV; VLEN = 128, so LMUL = 8 holds 8 blocks

  import_scc 0, a0
  vsetivli  zero, 16, e8, m1, ta, ma
  vle8.v    v1, (a5)
  kl.setst  K0, #kl_state_encrypt, v1  # IV <- v1
  kl.getst  t1, K0
  li        t2, kl_state_encrypt
  bne       t1, t2, error         # the import failed

  li        t3, 8                 # blocks per group
  beqz      a3, 2f
1:
  minu      t4, a3, t3            # blocks in this group: 8, then the rest
  slli      t5, t4, 4             # bytes: 16 * blocks
  vsetvli   zero, t5, e8, m8, ta, ma # KLLEN = 128 * blocks
  vle8.v    v8, (a1)
  kl.exec   v8, K0, v8            # each block chained to the last
  vse8.v    v8, (a2)
  add       a1, a1, t5
  add       a2, a2, t5
  sub       a3, a3, t4
  bnez      a3, 1b
2:

A CFB CC takes its segment size u from the AuxInfo field of its MDH, which is fixed at provisioning. The following listing encrypts with CFB8, that is with an SCC whose u = 8, any number of bytes.

CFB8 Encryption
# a0 = SCC, a1 = plaintext, a2 = ciphertext buffer, a3 = length in bytes,
# a5 = the 16-byte IV

  import_scc 0, a0                # u = 8, from the MDH
  vsetivli  zero, 16, e8, m1, ta, ma
  vle8.v    v1, (a5)
  kl.setst  K0, #kl_state_encrypt, v1       # IV <- v1
  kl.getst  t1, K0
  li        t2, kl_state_encrypt
  bne       t1, t2, error         # the import failed

  li        t3, 128               # a full register group
  beqz      a3, 2f
1:
  minu      t4, a3, t3            # whole segments: one byte each
  vsetvli   zero, t4, e8, m8, ta, ma
  vle8.v    v8, (a1)
  kl.exec   v8, K0, v8
  vse8.v    v8, (a2)
  add       a1, a1, t4
  add       a2, a2, t4
  sub       a3, a3, t4
  bnez      a3, 1b
2:

CBC and CFB8 decryption differ from the listings above only in the State: kl_state_decrypt replaces kl_state_encrypt. An OFB CC (State Operate) is used as in CTR/XCTR Keystream Generation, with Form C kl.exec instructions that return keystream blocks, except that OFB has no counter, so the kl.setst into Set_Aux_Value is omitted.

CTR/XCTR Keystream Generation

Encrypt with Keystream Generation
# a0 = SCC, a1 = PT, a2 = CT (may equal a1), a3 = length of PT in bytes,
# a4 = initial counter value, a5 = the IV: 16 bytes in XCTR mode, 12 in CTR

  import_scc 0, a0
  vsetivli  zero, 16, e8, m1, ta, ma # one block: KLLEN = 128
  vle8.v    v1, (a5)              # XCTR; for CTR use vl = 12 (KLLEN = 96)
  kl.setst  K0, #kl_state_operate, v1        # set the IV
  kl.setst  K0, #kl_state_set_aux_value, a4  # ctr <- bin(a4, j)

  li        t2, 16
  beqz      a3, 2f
1:
  vsetivli  zero, 16, e8, m1, ta, ma
  kl.exec   v1, K0                # a whole keystream block, also for a
                                  # final partial block
  minu      t1, a3, t2            # bytes of PT in this block
  vsetvli   zero, t1, e8, m1, ta, ma
  vle8.v    v0, (a1)
  vxor.vv   v1, v1, v0            # the keystream past the end of PT
  vse8.v    v1, (a2)              # is discarded
  add       a1, a1, t1
  add       a2, a2, t1
  sub       a3, a3, t1
  bnez      a3, 1b
2:

XEX/XTS Modes

The listings below encrypt and decrypt a data unit of n blocks, b = KLLEN = 128. The parameter last_len is the length of the last block in bytes: if it is 16, the data unit consists of whole blocks; otherwise the last block is partial, and the data unit is completed by XTS ciphertext stealing.

XEX Encryption
# a0 = SCC, a1 = PT, a2 = CT (may equal a1), a3 = n blocks, the last of
# a4 = last_len bytes, 1 <= last_len <= 16, n >= 2 if last_len < 16,
# a5 = the 16-byte tweak

  import_scc 0, a0
  vsetivli  zero, 16, e8, m1, ta, ma # one block: KLLEN = 128
  vle8.v    v0, (a5)
  kl.setst  K0, #kl_state_encrypt, v0 # configure to encrypt, set the tweak

  li        t1, 16
  mv        t2, a3                # m = n full blocks if last_len = 16,
  beq       a4, t1, 1f
  addi      t2, a3, -1            # n-1 otherwise
1:
  vle8.v    v0, (a1)
  kl.exec   v0, K0, v0            # encrypt one block of plaintext
  vse8.v    v0, (a2)
  addi      a1, a1, 16
  addi      a2, a2, 16
  addi      t2, t2, -1
  bnez      t2, 1b
  beq       a4, t1, 2f            # whole blocks: done

  # Ciphertext stealing: a1 points to P_m, a2 to C_m, s = last_len
  addi      t3, a2, -16           # C_{m-1}
  vle8.v    v0, (t3)              # CC, at mask index m-1
  vmv.v.v   v1, v0
  vsetvli   zero, a4, e8, m1, tu, ma # s bytes, the rest of v1 kept
  vle8.v    v1, (a1)              # v1 = CP @ P_m
  vse8.v    v0, (a2)              # C_m: the first s bytes of CC
  vsetivli  zero, 16, e8, m1, ta, ma
  kl.exec   v1, K0, v1            # C_{m-1}, at mask index m
  vse8.v    v1, (t3)
2:

Pm is read before Cm is written, so CT may be the same buffer as PT.

XEX Decryption
# a0 = SCC, a1 = CT, a2 = PT (may equal a1), a3 = n blocks, the last of
# a4 = last_len bytes, 1 <= last_len <= 16, n >= 2 if last_len < 16,
# a5 = the 16-byte tweak

  import_scc 0, a0
  vsetivli  zero, 16, e8, m1, ta, ma # one block: KLLEN = 128
  vle8.v    v0, (a5)
  kl.setst  K0, #kl_state_decrypt, v0 # configure to decrypt, set the tweak

  li        t1, 16
  mv        t2, a3                # n plain blocks if last_len = 16,
  beq       a4, t1, 1f
  addi      t2, a3, -2            # m-1 = n-2 otherwise
1:
  beqz      t2, 3f
2:
  vle8.v    v0, (a1)
  kl.exec   v0, K0, v0            # decrypt one block of ciphertext
  vse8.v    v0, (a2)
  addi      a1, a1, 16
  addi      a2, a2, 16
  addi      t2, t2, -1
  bnez      t2, 2b
3:
  beq       a4, t1, 4f            # whole blocks: done

  # Ciphertext stealing: a1 points to C_{m-1}, a2 to P_{m-1}, s = last_len
  kl.setst  K0, #kl_xex_state_steal # skip mask index m-1
  vle8.v    v0, (a1)              # C_{m-1}
  kl.exec   v0, K0, v0            # PP, at mask index m
  vmv.v.v   v1, v0
  addi      t3, a1, 16            # C_m
  addi      t4, a2, 16            # P_m
  vsetvli   zero, a4, e8, m1, tu, ma # s bytes, the rest of v1 kept
  vle8.v    v1, (t3)              # v1 = CP @ C_m
  vse8.v    v0, (t4)              # P_m: the first s bytes of PP
  vsetivli  zero, 16, e8, m1, ta, ma
  kl.exec   v1, K0, v1            # P_{m-1}, at mask index m-1
  vse8.v    v1, (a2)              # the locker is now in Success
4:

As in encryption, Cm is read before Pm is written. With stealing, the locker ends in Success, and a kl.setst into Ready precedes the next tweak.

GCM Mode

GCM Encryption encrypts with GCM, b = 128 (one block per kl.exec in the loops), c = 32.

Associated data and plaintext/ciphertext are treated differently, as GCM Mode requires. The caller zero-fills the final block of the AD beyond its actual end, and GHASH absorbs that block whole. The PT and CT are not zero-padded: a final block shorter than b goes through State Enc_Last_Block (or Dec_Last_Block), so no keystream bit past the end of the message is absorbed into GHASH or written out. The Machine takes PT and CT in whole bytes (each last-block Xs is a multiple of 8); these listings also take the AD in whole bytes, so len_in_bits is always 8 times a byte count.

GCM Encryption
# a0 = CC, a1 = AD, a2 = length of AD in bytes, a3 = PT, a4 = CT (may
# equal a3), a5 = length of PT in bytes, a6 = IV, a7 = length of IV in
# bytes, 1 to 1024.  The caller has zero-filled the last block of the AD.

  import_scc 0, a0                # also computes the derived hash key

  # Set the IV.  The Form B kl.setst carries its length in bits; kl.exec
  # then absorbs the IV in chunks of 16 bytes, the last one possibly
  # shorter.  After the last byte the Machine computes J0 and moves to
  # _Hash_Absorb_ by itself, so no kl.setst is issued for that transition.
  slli      t1, a7, 3
  kl.setst  K0, #kl_state_set_aux_value, t1 # len_in_bits(IV)
  li        t2, 16
1:
  minu      t1, a7, t2
  vsetvli   zero, t1, e8, m1, ta, ma # KLLEN = 8 * bytes in this chunk
  vle8.v    v0, (a6)
  kl.exec   K0, v0                # absorb a chunk of the IV
  add       a6, a6, t1
  sub       a7, a7, t1
  bnez      a7, 1b

  # The AD contribution to the tag
  vsetivli  zero, 16, e8, m1, ta, ma # one block: KLLEN = 128
  addi      t1, a2, 15
  srli      t1, t1, 4             # ceil(len_in_bytes(AD)/16) blocks
  mv        t3, a1
  beqz      t1, 3f
2:
  vle8.v    v0, (t3)
  kl.exec   K0, v0                # absorb it into GHASH
  addi      t3, t3, 16
  addi      t1, t1, -1
  bnez      t1, 2b
3:
  # Encrypt the whole blocks of the plaintext
  kl.setst  K0, #kl_state_encrypt
  srli      t1, a5, 4             # whole blocks of PT
  mv        t3, a3
  mv        t4, a4
  beqz      t1, 5f
4:
  vle8.v    v0, (t3)
  kl.exec   v1, K0, v0
  vse8.v    v1, (t4)
  addi      t3, t3, 16
  addi      t4, t4, 16
  addi      t1, t1, -1
  bnez      t1, 4b
5:
  # Final partial block, if any.  The plaintext is not zero-padded here.
  andi      t1, a5, 15            # its length in bytes
  beqz      t1, 6f
  slli      t2, t1, 3
  kl.setst  K0, #kl_state_enc_last_block, t2 # its length in bits
  vsetvli   zero, t1, e8, m1, ta, ma
  vle8.v    v0, (t3)
  kl.exec   v1, K0, v0
  vse8.v    v1, (t4)
6:
  # Absorb the lengths, finalize the tag, and read it out
  slli      t1, a5, 3
  rev8      t1, t1                # bswap(bin(len_in_bits(PT), 64))
  slli      t2, a2, 3
  rev8      t2, t2                # bswap(bin(len_in_bits(AD), 64))
  vsetivli  zero, 2, e64, m1, tu, ma # KLLEN = 128
  vmv.v.x   v1, t1                # bits [127:64]
  vmv.s.x   v1, t2                # bits [63:0]
  kl.setst  K0, #kl_state_enc_tag_finalize, v1
  kl.exec   v2, K0                # v2 holds the tag, of tag_len_p bits

GCM Decryption decrypts with GCM from the point where the AD has been absorbed; up to that point, it is GCM Encryption. No kl.exec is issued in State Dec_Tag_Finalize: the recomputed tag stays inside the locker.

GCM Decryption
# As GCM Encryption up to label 3, with a3 = CT, a4 = PT (may equal a3),
# a5 = length of CT in bytes, s10 = tag length in bits (128, 120, 112, 104, 96 or 64),
# and s9 = address of the received tag

  # Decrypt the whole blocks of the ciphertext
  kl.setst  K0, #kl_state_decrypt
  srli      t1, a5, 4             # whole blocks of CT
  mv        t3, a3
  mv        t4, a4
  beqz      t1, 5f
4:
  vle8.v    v0, (t3)
  kl.exec   v1, K0, v0
  vse8.v    v1, (t4)
  addi      t3, t3, 16
  addi      t4, t4, 16
  addi      t1, t1, -1
  bnez      t1, 4b
5:
  # Final partial block, if any.  The ciphertext is not zero-padded here.
  andi      t1, a5, 15
  beqz      t1, 6f
  slli      t2, t1, 3
  kl.setst  K0, #kl_state_dec_last_block, t2
  vsetvli   zero, t1, e8, m1, ta, ma
  vle8.v    v0, (t3)
  kl.exec   v1, K0, v0
  vse8.v    v1, (t4)
6:
  # Absorb the lengths.  The tag is computed and kept inside the locker.
  slli      t1, a5, 3
  rev8      t1, t1                # bswap(bin(len_in_bits(CT), 64))
  slli      t2, a2, 3
  rev8      t2, t2                # bswap(bin(len_in_bits(AD), 64))
  vsetivli  zero, 2, e64, m1, tu, ma
  vmv.v.x   v1, t1
  vmv.s.x   v1, t2
  kl.setst  K0, #kl_state_dec_tag_finalize, v1

  # Set the tag length in bits, equal to the locker's tag_len_p, supply the received tag; the comparison sets _Success_ or _Failure_
  kl.setst  K0, #kl_state_hash_verify, s10
  srli      t1, s10, 3
  vsetvli   zero, t1, e8, m1, ta, ma # KLLEN = tag length
  vle8.v    v1, (s9)
  kl.exec   K0, v1
  kl.getst  t1, K0
  li        t2, kl_state_success
  bne       t1, t2, error         # error must discard the plaintext at a4 (Rule SR18)

GCM encryption can also use an ECB CC, if the vector ISA has carry-less multiplication, namely the vghsh.vv of Zvkg. Under the threat model of KLE, however, this exposes the hashing key and may lead to forgeries, though not to a loss of confidentiality.

Implementing GCM encryption using an ECB CC
# a0 = ECB encryption CC, a1 = AD, a2 = length of AD in bytes, a3 = PT,
# a4 = CT (may equal a3), a5 = length of PT in bytes, a6 = the 12-byte IV;
# a1, a3, a4 and a6 are 4-byte aligned.
# The caller has zero-filled the last block of the AD.  vghsh.vv (Zvkg)
# computes vd <- (vd xor vs1) * vs2 in GF(2^128), as GHASH does.

  import_scc 0, a0
  kl.setst  K0, #kl_state_encrypt # kl.exec encrypts only in _Encrypt_
  vsetivli  zero, 4, e32, m1, ta, ma # one block: KLLEN = 128
  vmv.v.i   v0, 0
  vsetivli  zero, 3, e32, m1, tu, ma
  vle32.v   v0, (a6)              # v0[95:0] = IV, in bytes 0..11
  vsetivli  zero, 4, e32, m1, ta, ma
  vmv.v.i   v3, 0                 # zeros(b): input of H, GHASH start
  kl.exec   v4, K0, v3            # compute H

  addi      t1, a2, 15
  srli      t1, t1, 4             # ceil(len_in_bytes(AD)/16) blocks
  mv        t3, a1
  beqz      t1, 2f
1:
  vle32.v   v1, (t3)
  vghsh.vv  v3, v4, v1            # v3 <- (v3 xor v1) * H
  addi      t3, t3, 16
  addi      t1, t1, -1
  bnez      t1, 1b
2:
  # Counter 1 gives E(K, J0), the tag mask: with a 96-bit IV, the counter
  # of the text starts at 2.
  li        t5, 2
  srli      t1, a5, 4             # whole blocks of PT
  mv        t3, a3
  mv        t4, a4
  beqz      t1, 4f
3:
  rev8      t2, t5
  srli      t2, t2, 32            # bswap(bin(counter, 32))
  vmv.v.v   v5, v0
  vmv.s.x   v6, t2
  vslideup.vi v5, v6, 3           # v5 = bswap(bin(counter, 32)) @ IV
  vle32.v   v1, (t3)
  kl.exec   v2, K0, v5            # the mask: the encrypted counter
  vxor.vv   v1, v1, v2            # CTR encryption
  vghsh.vv  v3, v4, v1
  vse32.v   v1, (t4)
  addi      t3, t3, 16
  addi      t4, t4, 16
  addi      t5, t5, 1
  addi      t1, t1, -1
  bnez      t1, 3b
4:
  # Final partial block of r bytes, if any.  The ciphertext is truncated
  # to r bytes before it is absorbed and stored: GHASH absorbs the
  # zero-padded ciphertext, which is what SP 800-38D specifies.
  andi      t6, a5, 15            # r
  beqz      t6, 5f
  rev8      t2, t5
  srli      t2, t2, 32
  vmv.v.v   v5, v0
  vmv.s.x   v6, t2
  vslideup.vi v5, v6, 3
  kl.exec   v2, K0, v5
  vmv.v.i   v1, 0
  vsetvli   zero, t6, e8, m1, tu, ma # r bytes, the rest of v1 kept zero
  vle8.v    v1, (t3)
  vxor.vv   v1, v1, v2            # the keystream past the end is discarded
  vse8.v    v1, (t4)
  vsetivli  zero, 4, e32, m1, ta, ma
  vghsh.vv  v3, v4, v1
5:
  slli      t1, a5, 3
  rev8      t1, t1                # bswap(bin(len_in_bits(PT), 64))
  slli      t2, a2, 3
  rev8      t2, t2                # bswap(bin(len_in_bits(AD), 64))
  vsetivli  zero, 2, e64, m1, tu, ma
  vmv.v.x   v1, t1
  vmv.s.x   v1, t2
  vsetivli  zero, 4, e32, m1, ta, ma
  vghsh.vv  v3, v4, v1
  li        t2, 0x01000000        # bswap(bin(1, 32))
  vmv.v.v   v5, v0
  vmv.s.x   v6, t2
  vslideup.vi v5, v6, 3           # J0 (counter 1), for the tag mask
  kl.exec   v2, K0, v5
  vxor.vv   v3, v3, v2            # v3 holds the tag

GCM-SIV Mode

GCM-SIV Encryption encrypts with a GCM-SIV CC (b = 128, one block per kl.exec in the loops), for AD and plaintext in whole bytes, as the Machine requires; GCM-SIV Decryption decrypts.

GCM-SIV Encryption
# a0 = SCC, a1 = AD, a2 = length of AD in bytes, a3 = PT, a4 = CT (may
# equal a3), a5 = length of PT in bytes, a6 = the 12-byte nonce.  The
# caller has zero-padded the last block of the AD and of the PT.

  import_scc 0, a0

  # Set the nonce, which also derives enc_key and auth_key.  The nonce is
  # the vector operand of a Form C kl.setst; no kl.exec is issued in this
  # State.
  vsetivli  zero, 12, e8, m1, ta, ma # KLLEN = 96
  vle8.v    v0, (a6)
  kl.setst  K0, #kl_state_set_aux_value, v0

  # Absorb the AD and then the plaintext into POLYVAL
  vsetivli  zero, 16, e8, m1, ta, ma # one block: KLLEN = 128
  kl.setst  K0, #kl_state_hash_absorb
  addi      t1, a2, 15
  srli      t1, t1, 4             # ceil(len_in_bytes(AD)/16) blocks
  mv        t3, a1
  beqz      t1, 2f
1:
  vle8.v    v0, (t3)              # a block of associated data
  kl.exec   K0, v0                # absorb it into POLYVAL
  addi      t3, t3, 16
  addi      t1, t1, -1
  bnez      t1, 1b
2:
  addi      t1, a5, 15
  srli      t1, t1, 4             # ceil(len_in_bytes(PT)/16) blocks
  mv        t3, a3
  beqz      t1, 4f
3:
  vle8.v    v0, (t3)              # a block of plaintext
  kl.exec   K0, v0                # absorb it into POLYVAL
  addi      t3, t3, 16
  addi      t1, t1, -1
  bnez      t1, 3b
4:
  # Finalize the SIV.  The lengths are the *input* of a Form A kl.exec in
  # State _Enc_Tag_Finalize_, and the SIV is its output; they are not
  # absorbed by a further kl.exec in _Hash_Absorb_.
  kl.setst  K0, #kl_state_enc_tag_finalize
  slli      t1, a5, 3
  slli      t2, a2, 3
  vsetivli  zero, 2, e64, m1, tu, ma # KLLEN = 128
  vmv.v.x   v0, t1                # bin(len_in_bits(PT), 64)
  vmv.s.x   v0, t2                # @ bin(len_in_bits(AD), 64)
  kl.exec   v2, K0, v0            # v2 holds the tag

  # Second pass: encrypt the whole blocks of the plaintext, with the SIV
  # as IV.  The kl.exec above has already moved the locker to _Encrypt_;
  # a kl.setst naming _Encrypt_ would invalidate it.
  vsetivli  zero, 16, e8, m1, ta, ma
  srli      t1, a5, 4             # whole blocks of PT
  mv        t3, a3
  mv        t4, a4
  beqz      t1, 6f
5:
  vle8.v    v0, (t3)              # a block of plaintext
  kl.exec   v1, K0, v0            # encrypt it
  vse8.v    v1, (t4)              # a block of ciphertext
  addi      t3, t3, 16
  addi      t4, t4, 16
  addi      t1, t1, -1
  bnez      t1, 5b
6:
  # Final partial block, if any.  Unlike in the first pass, it must not
  # go through as a zero-padded whole block: _Enc_Last_Block_ writes no
  # keystream bit past the end of PT to CT.
  andi      t1, a5, 15
  beqz      t1, 7f
  slli      t2, t1, 3
  kl.setst  K0, #kl_state_enc_last_block, t2
  vsetvli   zero, t1, e8, m1, ta, ma
  vle8.v    v0, (t3)
  kl.exec   v1, K0, v0
  vse8.v    v1, (t4)
7:
GCM-SIV Decryption
# a0 = SCC, a1 = AD, a2 = length of AD in bytes, a3 = CT, a4 = PT (may
# equal a3), a5 = length of CT in bytes, a6 = the 12-byte nonce, a7 = the
# 16-byte received SIV.  The caller has zero-padded the last block of AD.

  import_scc 0, a0

  # Set the nonce
  vsetivli  zero, 12, e8, m1, ta, ma # KLLEN = 96
  vle8.v    v0, (a6)
  kl.setst  K0, #kl_state_set_aux_value, v0

  # Set the received SIV, which is the IV of the decryption
  vsetivli  zero, 16, e8, m1, ta, ma # one block: KLLEN = 128
  vle8.v    v0, (a7)
  kl.setst  K0, #kl_state_set_aux_value_2, v0

  # Absorb the AD
  kl.setst  K0, #kl_state_hash_absorb
  addi      t1, a2, 15
  srli      t1, t1, 4             # ceil(len_in_bytes(AD)/16) blocks
  mv        t3, a1
  beqz      t1, 2f
1:
  vle8.v    v0, (t3)
  kl.exec   K0, v0                # absorb it into POLYVAL
  addi      t3, t3, 16
  addi      t1, t1, -1
  bnez      t1, 1b
2:
  # Decrypt the whole blocks; each recovered block is absorbed into POLYVAL
  kl.setst  K0, #kl_state_decrypt
  srli      t1, a5, 4             # whole blocks of CT
  mv        t3, a3
  mv        t4, a4
  beqz      t1, 4f
3:
  vle8.v    v0, (t3)
  kl.exec   v1, K0, v0
  vse8.v    v1, (t4)
  addi      t3, t3, 16
  addi      t4, t4, 16
  addi      t1, t1, -1
  bnez      t1, 3b
4:
  # Final partial block, if any; without one, bypass Dec_Last_Block
  andi      t1, a5, 15
  bnez      t1, 6f
  kl.setst  K0, #kl_state_dec_tag_finalize  # Form A bypass
  j         5f
6:
  slli      t2, t1, 3
  kl.setst  K0, #kl_state_dec_last_block, t2
  vsetvli   zero, t1, e8, m1, ta, ma
  vle8.v    v0, (t3)
  kl.exec   v1, K0, v0            # also enters Dec_Tag_Finalize
  vse8.v    v1, (t4)
5:
  # Absorb the lengths and verify, with a Form B kl.exec: the recomputed
  # SIV is compared internally against the one supplied above
  slli      t1, a5, 3
  slli      t2, a2, 3
  vsetivli  zero, 2, e64, m1, tu, ma # KLLEN = 128
  vmv.v.x   v0, t1                # bin(len_in_bits(PT), 64)
  vmv.s.x   v0, t2                # @ bin(len_in_bits(AD), 64)
  kl.exec   K0, v0
  kl.getst  t1, K0
  li        t2, kl_state_success
  bne       t1, t2, error         # error must discard the plaintext at a4 (Rule SR18)

OCB Mode

This section gives OCB encryption and decryption (b = 128, one block per kl.exec in the loops).

OCB Encryption
# a0 = CC, a1 = AD, a2 = AD_len in bits, a3 = PT, a4 = CT (may equal a3),
# a5 = PT_len in bits, a6 = nonce N, a7 = N_len in bits, s9 = tag_len.
# AD_len and PT_len are multiples of 8.

  import_scc 0, a0

  # Set the nonce
  kl.setst  K0, #kl_state_set_aux_value, a7
  addi      t1, a7, 7
  srli      t1, t1, 3             # ceil(N_len/8) bytes
  vsetvli   zero, t1, e8, m1, ta, ma
  vle8.v    v0, (a6)
  kl.exec   K0, v0

  # The next state change also sets the tag length
  kl.setst  K0, #kl_state_hash_absorb, s9

  # Compute the AD contribution to the tag
  vsetivli  zero, 16, e8, m1, ta, ma # one block: KLLEN = 128
  srli      t1, a2, 7             # whole blocks of AD
  mv        t3, a1
  beqz      t1, 2f
1:
  vle8.v    v0, (t3)
  kl.exec   K0, v0                # absorb it into the hash
  addi      t3, t3, 16
  addi      t1, t1, -1
  bnez      t1, 1b
2:
  andi      t2, a2, 127           # can be zero
  kl.setst  K0, #kl_state_hash_last_block, t2
  beqz      t2, 3f
  srli      t1, t2, 3
  vsetvli   zero, t1, e8, m1, ta, ma
  vle8.v    v0, (t3)
  kl.exec   K0, v0                # absorb the last AD block
3:
  # Encrypt the plaintext and continue the computation of the tag
  kl.setst  K0, #kl_state_encrypt
  vsetivli  zero, 16, e8, m1, ta, ma
  srli      t1, a5, 7             # whole blocks of PT
  mv        t3, a3
  mv        t4, a4
  beqz      t1, 5f
4:
  vle8.v    v0, (t3)
  kl.exec   v1, K0, v0
  vse8.v    v1, (t4)
  addi      t3, t3, 16
  addi      t4, t4, 16
  addi      t1, t1, -1
  bnez      t1, 4b
5:
  andi      t2, a5, 127           # can be zero
  kl.setst  K0, #kl_state_enc_last_block, t2
  beqz      t2, 6f
  srli      t1, t2, 3
  vsetvli   zero, t1, e8, m1, ta, ma
  vle8.v    v0, (t3)
  kl.exec   v1, K0, v0            # encrypt it and absorb it into the tag
  vse8.v    v1, (t4)
  j         7f
6:
  kl.exec   K0                    # Form D: finalize the tag only
7:
  # _Enc_Last_Block_ transitions to _Enc_Tag_Finalize_ on its own, so no
  # kl.setst is issued here.
  vsetivli  zero, 16, e8, m1, ta, ma
  kl.exec   v1, K0                # v1 holds the tag, tag_len bits

OCB decryption follows OCB Encryption up to label 7, with the roles of PT and CT exchanged, and States Decrypt and Dec_Last_Block in place of Encrypt and Enc_Last_Block; OCB Decryption gives only the differing last step: OCB has no Dec_Tag_Finalize State, and after the last block, Dec_Last_Block transitions directly to Hash_Verify, where a Form B kl.exec supplies the received tag.

OCB Decryption
# s9 = tag_len in bits, s10 = address of the received tag
  srli      t1, s9, 3
  vsetvli   zero, t1, e8, m1, ta, ma # KLLEN = tag_len
  vle8.v    v1, (s10)
  kl.exec   K0, v1                # Form B: compares it with checksum_P
  kl.getst  t1, K0
  li        t2, kl_state_success
  bne       t1, t2, error         # error must discard the plaintext at a4 (Rule SR18)

CMAC Functions

CMAC Computation
# a0 = CC with b = 128 (AES, SM4), a1 = CMAC input M,
# a2 = M_len in bits, a multiple of 8

  import_scc 0, a0
  vsetivli  zero, 16, e8, m1, ta, ma # one block: KLLEN = 128

  kl.setst  K0, #kl_state_hash_absorb
  srli      t1, a2, 7             # blocks
  andi      t2, a2, 127           # remainder
  bnez      t2, 1f
  beqz      t1, 1f
  addi      t1, t1, -1            # a whole number of full blocks: the
  li        t2, 128               # last one is the "last block", and a
1:                                # full last block takes no 10* padding
  beqz      t1, 3f
2:
  vle8.v    v0, (a1)
  kl.exec   K0, v0
  addi      a1, a1, 16
  addi      t1, t1, -1
  bnez      t1, 2b
3:
  kl.setst  K0, #kl_state_hash_last_block, t2
                                  # with t2 = 0 (empty message), the
                                  # padded block is absorbed here
  beqz      t2, 4f
  srli      t1, t2, 3
  vsetvli   zero, t1, e8, m1, ta, ma
  vle8.v    v0, (a1)
  kl.exec   K0, v0                # absorb the last block
  vsetivli  zero, 16, e8, m1, ta, ma
4:
  kl.setst  K0, #kl_state_hash_output
  kl.exec   v0, K0                # v0 holds the CMAC, of tag_len_p bits

To verify a CMAC T of T_len bits instead, the code from label 4 on is replaced by:

CMAC Verification
# a3 = T, a4 = T_len in bits: the locker's tag_len_p, a multiple of 8, 64 to 128
4:
  kl.setst  K0, #kl_state_hash_verify, a4
  srli      t1, a4, 3
  vsetvli   zero, t1, e8, m1, ta, ma # KLLEN = T_len
  vle8.v    v0, (a3)
  kl.exec   K0, v0                # compares the first T_len bits
  kl.getst  t1, K0                # _Success_ if T matches,
                                  # _Failure_ otherwise

Hash Functions, MACs and XOFs

The following sequence hashes a message with any Machine of Hash and MAC Functions, and XOFs; only the SCC and out_bytes, the number of output bytes, differ.

  • Message. The caller passes M unpadded; the kl.setst that ends it pads it. The final piece, of M_bits mod 64 bits, fits every Machine, since b ≥ 64; for BLAKE2 it is a whole number of bytes. A partial final byte holds the final bits as the standard orders them: in its most significant positions for SHA-2 and SM3, in its least significant ones for SHA-3, KMAC and Ascon, and as its H does for HMAC.

  • Output length. Xs = 8 × out_bytes in the kl.setst into Hash_Output makes the Machine emit exactly out_bytes bytes, then enter Success. For KMAC128 and KMAC256 it is also the L that the MAC encodes, while KMACXOF128 and KMACXOF256 encode L = 0; for every other Machine it only truncates the output. Xs = 0 requests the full digest, or an unbounded XOF, and is invalid for KMAC128 and KMAC256. out_bytes is at least 1 (8 for HMAC, 4 for KMAC128 and KMAC256), at most the digest length for a fixed-output Machine, and below 229 for an XOF, KMAC128 or KMAC256.

  • BLAKE2. A digest of nn ≠ nnmax bytes, such as BLAKE2b-256, needs an SCC whose nn, which enters the IV, was set by the Set_Aux_Value operation of BLAKE2 Specific Rules or by a system-specific import. With kk ≠ 0 and no system key, M starts with the key zero-padded to b bits (cite:[RFC7693] §3.3), which alone forms the message for an empty M; a system key needs no such block.

  • The SCC is in Ready, or in Hash_Absorb if it was exported after a prefix of the message, such as secret content (NOTE of Ascon-Hash256); it then takes no first kl.setst, and M is the rest of the message. An SCC of the NIK variant of HMAC must already hold its key. An SCC of HMAC, KMAC128, KMAC256, or BLAKE2 with a system key must enable Hash_Output (MachinePolicy[0]); for the MAC verification below, one of HMAC, KMAC128 or KMAC256 must enable Hash_Verify (MachinePolicy[1]) instead.

Hashing a Message
# a0 = SCC of any hash, MAC or XOF, a1 = message M, a2 = M_bits,
# a3 = output buffer OUT, a4 = out_bytes

  import_scc 0, a0
  kl.setst  K0, #kl_state_hash_absorb

  andi      t3, a2, 63            # r: the final piece, 0 to 63 bits
  sub       t4, a2, t3
  srli      t4, t4, 3             # h: the bytes before it
  li        t2, 16
  beqz      t4, 2f
1:
  minu      t1, t4, t2            # the last chunk may be shorter
  vsetvli   zero, t1, e8, m1, ta, ma # KLLEN = 8 * t1
  vle8.v    v0, (a1)
  kl.exec   K0, v0                # absorb t1 bytes, unpadded
  add       a1, a1, t1
  sub       t4, t4, t1
  bnez      t4, 1b
2:
  kl.setst  K0, #kl_state_hash_last_block, t3 # r = 0: does nothing
  beqz      t3, 3f
  addi      t1, t3, 7
  srli      t1, t1, 3             # ceil(r/8) bytes
  vsetvli   zero, t1, e8, m1, ta, ma
  vle8.v    v0, (a1)
  kl.exec   K0, v0                # absorb the final r bits
3:
  slli      t1, a4, 3
  kl.setst  K0, #kl_state_hash_output, t1 # end the message: the Machine
                                  # pads
4:
  minu      t1, a4, t2
  vsetvli   zero, t1, e8, m1, ta, ma
  kl.exec   v0, K0                # emit the next t1 bytes
  vse8.v    v0, (a3)
  add       a3, a3, t1
  sub       a4, a4, t1
  bnez      a4, 4b
  kl.getst  t1, K0                # _Success_

To verify a MAC T of T_len bits with HMAC, KMAC128 or KMAC256 instead, the code from label 3 on is replaced by the listing below. For HMAC, min(T_len, t) must equal the locker’s tag_len_p (Rule MR12), and the listing compares that many bits. For KMAC128 and KMAC256, it takes T_len ≤ b, and T_len ≤ 1024 if VLEN = 128, since a register group with LMUL = 8 holds 8 × VLEN bits.

MAC Verification
# a5 = T, a6 = T_len in bits, a multiple of 8, >= 64 (KMAC: 32); for HMAC, the locker's tag_len_p
3:
  kl.setst  K0, #kl_state_hash_verify, a6
  srli      t1, a6, 3
  vsetvli   zero, t1, e8, m8, ta, ma # KLLEN = T_len
  vle8.v    v0, (a5)
  kl.exec   K0, v0                # compares the first T_len bits
  kl.getst  t1, K0                # _Success_ if T matches,
                                  # _Failure_ otherwise

With Ascon-CXOF128 and a customization string Z of Z_bits bits, the first kl.setst is replaced by:

Ascon-CXOF128 Customization
# a5 = Z, a6 = Z_bits, 0 to 2048
  kl.setst  K0, #kl_state_set_aux_value, a6 # absorbs bin(Z_bits, 64)
  addi      t4, a6, 7
  srli      t4, t4, 3             # ceil(Z_bits/8) bytes of Z
  li        t2, 16
  beqz      t4, 2f
1:
  minu      t1, t4, t2            # chunks of up to 16 bytes, as for M
  vsetvli   zero, t1, e8, m1, ta, ma
  vle8.v    v0, (a5)
  kl.exec   K0, v0
  add       a5, a5, t1
  sub       t4, t4, t1
  bnez      t4, 1b
2:
  kl.setst  K0, #kl_state_hash_absorb # pad Z; M follows as above

Key Agreement and Key Derivation

The following sequence runs an ephemeral P-256 key agreement, derives an AES-256 key K = SHA-256(00000001 ‖ Z ‖ FixedInfo) from its shared secret Z with the one-step key-derivation function of cite:[NIST-SP-800-56Cr2] §4.1, and places K in a GCM locker. Software sees neither Z nor K.

  • Lockers. K1 holds an ECC Machine for secp256r1, K0 a SHA-256 Machine and K2 a GCM Machine with AES-256 and the full tag_len_p, each in Ready. The ECC States have no named constants, so the listing writes them by number (Elliptic Curve Cryptography).

  • Points. The listing returns the ephemeral public key for the peer. The peer’s public key Q arrives encoded as Elliptic Curve Cryptography specifies: its X coordinate, then its Y coordinate, each 32 bytes little-endian. Point_Mul validates Q, and transitions to Failure if it is not a valid point.

  • The check after the first kl.derive. A kl.derive from a locker in Failure makes that locker Invalid and leaves K0 unchanged, so K would be derived without Z; the kl.getst that follows rejects that case.

  • FixedInfo takes at most 16 bytes here; a longer one is absorbed in chunks, as in Hashing a Message.

ECDH and One-Step Key Derivation into an AES-256 Key
# a0 = SCC of ECC secp256r1, a1 = SCC of SHA-256, a2 = SCC of GCM with AES-256,
# a3 = peer public key Q, 64 bytes, a4 = buffer for the ephemeral public key,
# 64 bytes, a5 = FixedInfo, a6 = its length in bytes, 1 to 16

  import_scc 1, a0
  import_scc 0, a1
  import_scc 2, a2
  li        t2, 1
  kl.setst  K1, #3, t2            # Set_Scalar, Xs != 0: a random scalar d
  kl.setst  K1, #4                # Point_Mul
  kl.exec   K1                    # Form D: SecondPt <- [d]G, to Output
  li        t1, 64
  vsetvli   zero, t1, e8, m4, ta, ma # KLLEN = 512: one point
  kl.exec   v0, K1                # emit [d]G, to Success
  vse8.v    v0, (a4)              # the ephemeral public key, for the peer
  kl.setst  K1, #kl_state_ready   # Form A keeps d
  kl.setst  K1, #8                # Set_SecondPt
  vle8.v    v0, (a3)
  kl.exec   K1, v0                # load Q, set HasSecondPt
  kl.setst  K1, #4                # Point_Mul
  kl.exec   K1                    # SecondPt <- [d]Q, to Output with a
                                  # shared secret, or Failure for a bad Q
  kl.setst  K0, #kl_state_hash_absorb
  lui       t1, 0x01000           # the counter 00000001, big-endian
  vsetivli  zero, 1, e32, m1, ta, ma # KLLEN = 32
  vmv.s.x   v0, t1
  kl.exec   K0, v0                # absorb the counter
  li        t1, 32
  kl.derive K0, K1, t1            # absorb Z, 32 bytes; K1 to Success
  kl.getst  t3, K1
  li        t4, 54
  bne       t3, t4, error         # K1 not in Success: Z did not enter K0
  vsetvli   zero, a6, e8, m1, ta, ma
  vle8.v    v0, (a5)
  kl.exec   K0, v0                # absorb FixedInfo
  kl.setst  K0, #kl_state_hash_output # pad: the digest is K
  kl.derive K2, K0, t1            # K into the AES-256 key; K0 to Success

Sealed variant. With MachineExtension = 2 in the ECC SCC, the same listing also keeps Z and K from software that controls the lockers (Rule GR31.3). The public key stays readable, but a Form C kl.exec of Z makes K1 Invalid, and Z seals K0 when it enters it, so a Form C kl.exec of the digest makes K0 Invalid. Both kl.derive instructions are then restricted: each moves at least 16 bytes, and each narrows the Metadata of its destination (Rule GR30), so that K2 ends with no wider a UsagePolicy, Locality or ExpirationDate than K1. K0 and K2 therefore need an SCProtection at least that of K1.

Longer keys. Z feeds one kl.derive (Rule GR28), sealed or not, so the counter cannot advance to 00000002. A key longer than the digest, or several keys, need a longer output from one hash locker: SHA-512, or KMAC256 with L set to the total length, S = "KDF" and an agreed salt of at most 131 bytes as K (Option 3 of cite:[NIST-SP-800-56Cr2] §4.1; the 132-byte default_salt spans two key_block blocks). Each kl.derive from that output takes its next bytes.

Rejection Sampling from an XOF

Hash_Output_Bits reads an XOF output a set number of bits at a time, which suits rejection sampling. The following sequence computes SampleNTT of cite:[nist-fips-203] Algorithm 7, with which ML-KEM expands its matrix from a seed. It reads the SHAKE128 output for B = ρ ‖ j ‖ i in 12-bit pieces, and keeps each value below q = 3329 until it has 256 coefficients.

  • SHAKE128 emits each output byte from its least significant bit, so consecutive 12-bit pieces are the values d1, d2 that Algorithm 7 forms from each 3 bytes, in order.

  • Each kl.setst sets read_len to 12. The first also ends the message, as a Form A kl.setst naming Hash_Output would, for an unbounded output.

  • The Form C kl.exec that follows, with KLLEN = 16, emits those 12 bits and clears the 4 above them, so vmv.x.s returns the value itself.

  • SampleNTT runs on public data, since ρ is part of the encapsulation key, so its variable number of reads reveals nothing secret.

SampleNTT from SHAKE128
# a0 = SCC of SHAKE128, a1 = B = rho || j || i, 34 bytes,
# a2 = output, 256 coefficients of 16 bits

  import_scc 0, a0
  kl.setst  K0, #kl_state_hash_absorb
  li        t1, 34
  vsetvli   zero, t1, e8, m4, ta, ma # KLLEN = 272
  vle8.v    v0, (a1)
  kl.exec   K0, v0                # absorb B
  vsetivli  zero, 1, e16, m1, ta, ma # KLLEN = 16
  li        t2, 12
  li        t3, 3329              # q
  addi      t4, a2, 512           # the end of the output
1:
  kl.setst  K0, #kl_state_hash_output_bits, t2 # read_len = 12; the first pads
  kl.exec   v0, K0                # the next 12 bits, zero-extended
  vmv.x.s   t1, v0
  bgeu      t1, t3, 1b            # reject a value >= q
  sh        t1, 0(a2)
  addi      a2, a2, 2
  bne       a2, t4, 1b