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 |
| SR2 |
Software should zero |
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 |
| SR4 |
Software should not write 0 to |
| SR5 |
Software that transfers an image through the KLIOBUF must save |
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 |
| SR7 |
While a locker is in State |
| 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 |
| 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:
|
| 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 |
Requirements on Transfers, Keys and Outputs
| SR14 |
Software must bound a sequence of |
| 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 |
| 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):
Before each instruction that Rule SR6 lists, it sets klnamedlocker as that Rule requires.
On exit, the handler:
-
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); -
restores the vector state, including
vstart; and -
restores
klstartlast (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, KLNLreads the State of the locker thatklnamedlockernames; withklnamedlocker= 32 it returns 0 (Rule GR13.2). -
To inspect lockers by number, software writes each number in turn to
klnamedlockerand namesKLNLin the following instructions. While an import or export is open, it first saves that locker by a nested export (Rule SR7). -
kl.getstreads the State,kl.getstxthe StateExtension, andkl.getmdthe whole MDH. -
On RV32,
kl.getstandkl.getstxwrite the GPR pairX[d+1] @ X[d], andkl.getmda 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
klnamedlockernames. It savesklnamedlocker; 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 lockerKLNLthroughout, 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 openingkl.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
-
Save
klstart(kl.getks). -
Save
klnamedlocker(kl.getnl). -
Let the managed locker be the locker that the saved
klnamedlockerindexes, if it is in [0..31]; otherwise, the locker in Statekl_cfg_importingorkl_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 Dkl.mgmt(Rule SR7), choosing the path by its State, read withkl.getst, and transferring through GPRs or vector registers, not the KLIOBUF, which step 4 has yet to save:-
Unconfigured: save no locker image;
-
an Error State: save its MDH by the short Error-State path (Export and Import of a Locker in an Error State);
-
otherwise, export the locker, by nested export if it is in a Configuration State.
-
-
With
Zklio:-
save
kliobuflenandkliobuftop(kl.getbl,kl.getbt); -
if
kliobuflenis non-zero, save the buffer contents withkl.output, withklnamedlocker= 32,klstart= 0 andkliobuftop=kliobuflen(kl.setnli 32,kl.setksi 0,kl.setbtof the saved length), then write 0 tokliobuflen(kl.setbli 0).
-
-
For every locker other than the managed locker, act by its
*llockerstatusfield 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. Keepklnamedlocker= 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
-
Clear the lockers for which no image is restored below, or all of them with
kl.clearall(Rule SR1). -
Restore the Locality Secret CSRs that the context owns, if any (Locality Secret CSR Groups), before any import of a locker that they bind.
-
Restore the lockers other than the managed locker (Context Save Order), with
klnamedlocker= 32 between imports. -
With
Zklio:-
restore
kliobuflen(kl.setbl), which zeroes the buffer and setskliobuftopto it; -
restore the buffer contents, if saved, with
kl.input, withklnamedlocker= 32,klstart= 0 andkliobuftop=kliobuflen; -
restore
kliobuftop(kl.setbt), after the buffer, since a write ofkliobuflenresets it.
-
-
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. -
Restore the saved value of
klnamedlocker, or 32 if the handler has cleared the locker it named instead of saving it. -
Restore
klstartafter every other KLE instruction (kl.setks). -
Write the
*llockerstatusfields 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 writeKLS, 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
t0holds the number of the locker to be provisioned, imported or exported, or 32 for the locker thatklnamedlockernames, and GPRt6points to the 16-byte aligned memory location of the PI or SCC (Rule GR12). The MDH is loaded and stored withldandsd, and passed in the GPR pairs5:s4; on RV32 it takes a GPR quartet (Rule GR2). - Selecting the locker
-
The routines encode no locker number: they save
klnamedlockeron entry, select the locker with the preambleselect_lockerbelow on entry and at everyrestart, and every KLE instruction then acts on that locker (Rules GR4 and GR4):kl.load,kl.store,kl.mvinandkl.mvouthave no locker operand, andkl.mgmt,kl.getst,kl.getmdandkl.sizenameKLNL. A clear resetsklnamedlockerto 32, after which aKLNLopening 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 atrestartnames lockert0again or, ift0= 32, branches tono_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.getstand, if it is 0, the StateExtension withkl.getstx. If a handler has meanwhile cleared the locker or set an Error State, every followingkl.mvin,kl.mvout,kl.load,kl.storeorkl.mgmt, including aKLNLopening or completion after a clear, performs no operation (Rule GR13.2), and the check detects the change:-
after a clear,
KLNLreads as Unconfigured, with State and StateExtension 0: a provisioning or an import restarts at its preamble, and an export, whose CC is destroyed, branches tohandle_errors; -
after an Error State, or after the completing
kl.mgmt,KLNLstill names the locker, so the check reads its final State and StateExtension; an Error State abandons the operation.
kl.loadandkl.storetakeXl=x0, so after a clear they are akl.inputorkl.outputwithXl= 0, which performs no operation (Rule GR11). The macrocheck_statemakes this check for provisioning and import; an export branches tohandle_errorson 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_errorsandno_lockeris not shown;no_lockerreturns an error to the caller, since witht0= 32 no locker is named, or a clear has destroyed the named one. If the saved value ofklnamedlockernames the locker that a clear has destroyed, both write 32 toklnamedlockerinstead of restoring it, as a handler that clears a locker instead of saving it restores 32 (Context Restore Order); this is always the case forno_lockerreached after a clear witht0= 32, and forhandle_errorsreached on an export whose locker has been cleared, if the caller had named that locker.handle_errorswrites 0 toklstart, which akl.mvinorkl.mvoutloop leaves at its last transfer point, and restoresklnamedlockeras the routines do on exit. If the locker is still in a Configuration State,handle_errorsclears 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 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
klnamedlockeron entry. Its clear setsklnamedlockerto 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 withklnamedlocker= 32. - The instructions of the routine
-
With
klnamedlocker= 32, the routine’s instructions find no locker (Rule GR13.2):-
kl.mvinand everykl.mgmtnamingKLNL, including the completing one, do nothing; -
kl.mvoutwrites zeros to its vector destination, which the export routine may store to memory before its next check; -
kl.loadandkl.storebecome akl.inputand akl.outputwithXl=x0, which do nothing (Rule GR11); -
the next check reads State 0:
check_statein a provisioning or an import, which also reads StateExtension 0, andkl.getstin an export.
-
- The next step of the routine
-
-
A provisioning or an import with
t0in 0–31 branches torestart. Thereselect_lockernames lockert0again, the openingkl.mgmtzeroizes and reopens it, and the operation starts over. -
A provisioning or an import with
t0= 32 branches torestart, whereselect_lockerreads 32 and branches tono_locker. The caller gets an error, because nothing records which locker it meant. -
An export branches to
handle_errorsat 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
klnamedlockerunchanged.KLNLwould 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 witht0in 0–31 restarts, as above. Witht0= 32, however, a provisioning or an import would miss the loss: atrestart,select_lockerwould find the number of the cleared locker inklnamedlocker, 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
KLNLon that context’s locker instead of falling through, and its checks would read that locker’s State.
Resetting
klnamedlockerto 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.
| Instruction and Form | Native Instruction | Substitution Sequence |
|---|---|---|
|
|
|
|
|
|
|
|
|
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.
# 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.
# 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.
# 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.
# 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.
# 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
# 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.
# 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.
# 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.
# 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.
# 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.
# 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.
# 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:
# 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).
# 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.
# 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
# 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:
# 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
Munpadded; thekl.setstthat ends it pads it. The final piece, ofM_bitsmod 64 bits, fits every Machine, sinceb≥ 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 itsHdoes for HMAC. -
Output length.
Xs= 8 ×out_bytesin thekl.setstinto Hash_Output makes the Machine emit exactlyout_bytesbytes, then enter Success. For KMAC128 and KMAC256 it is also theLthat the MAC encodes, while KMACXOF128 and KMACXOF256 encodeL= 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_bytesis 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≠nnmaxbytes, such as BLAKE2b-256, needs an SCC whosenn, which enters the IV, was set by the Set_Aux_Value operation of BLAKE2 Specific Rules or by a system-specific import. Withkk≠ 0 and no system key,Mstarts with the key zero-padded tobbits (cite:[RFC7693] §3.3), which alone forms the message for an emptyM; 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, andMis 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.
# 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.
# 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:
# 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.
K1holds an ECC Machine for secp256r1,K0a SHA-256 Machine andK2a GCM Machine with AES-256 and the fulltag_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
Qarrives encoded as Elliptic Curve Cryptography specifies: its X coordinate, then its Y coordinate, each 32 bytes little-endian. Point_Mul validatesQ, and transitions to Failure if it is not a valid point. -
The check after the first
kl.derive. Akl.derivefrom a locker in Failure makes that locker Invalid and leavesK0unchanged, soKwould be derived withoutZ; thekl.getstthat follows rejects that case. -
FixedInfo takes at most 16 bytes here; a longer one is absorbed in chunks, as in Hashing a Message.
# 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,d2that Algorithm 7 forms from each 3 bytes, in order. -
Each
kl.setstsetsread_lento 12. The first also ends the message, as a Form Akl.setstnaming Hash_Output would, for an unbounded output. -
The Form C
kl.execthat follows, withKLLEN= 16, emits those 12 bits and clears the 4 above them, sovmv.x.sreturns the value itself. -
SampleNTT runs on public data, since
ρis part of the encapsulation key, so its variable number of reads reveals nothing secret.
# 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