blob: 1546db613645d158d31bae889511a3c23c9226ff [file] [log] [blame] [edit]
// Copyright 2021 The BoringSSL Authors
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// testmodulewrapper is a modulewrapper binary that works with acvptool and
// implements the primitives that BoringSSL's modulewrapper doesn't, so that
// we have something that can exercise all the code in avcptool.
package main
import (
"bytes"
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/ed25519"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"errors"
"fmt"
"hash"
"io"
"os"
"filippo.io/edwards25519"
"golang.org/x/crypto/hkdf"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"
"golang.org/x/crypto/xts"
)
var (
output io.Writer
outputBuffer *bytes.Buffer
)
var handlers = map[string]func([][]byte) error{
"flush": flush,
"getConfig": getConfig,
"KDF-counter": kdfCounter,
"AES-XTS/encrypt": xtsEncrypt,
"AES-XTS/decrypt": xtsDecrypt,
"HKDF/SHA2-256": hkdfMAC,
"hmacDRBG-reseed/SHA2-256": hmacDRBGReseed,
"hmacDRBG-pr/SHA2-256": hmacDRBGPredictionResistance,
"AES-CBC-CS3/encrypt": ctsEncrypt,
"AES-CBC-CS3/decrypt": ctsDecrypt,
"PBKDF": pbkdf,
"EDDSA/keyGen": eddsaKeyGen,
"EDDSA/keyVer": eddsaKeyVer,
"EDDSA/sigGen": eddsaSigGen,
"EDDSA/sigVer": eddsaSigVer,
"SHAKE-128": shakeAftVot(sha3.NewShake128),
"SHAKE-128/VOT": shakeAftVot(sha3.NewShake128),
"SHAKE-128/MCT": shakeMct(sha3.NewShake128),
"SHAKE-256": shakeAftVot(sha3.NewShake256),
"SHAKE-256/VOT": shakeAftVot(sha3.NewShake256),
"SHAKE-256/MCT": shakeMct(sha3.NewShake256),
}
func flush(args [][]byte) error {
if outputBuffer == nil {
return nil
}
if _, err := os.Stdout.Write(outputBuffer.Bytes()); err != nil {
return err
}
outputBuffer = new(bytes.Buffer)
output = outputBuffer
return nil
}
func getConfig(args [][]byte) error {
if len(args) != 0 {
return fmt.Errorf("getConfig received %d args", len(args))
}
if err := reply([]byte(`[
{
"algorithm": "acvptool",
"features": ["batch"]
}, {
"algorithm": "KDF",
"revision": "1.0",
"capabilities": [{
"kdfMode": "counter",
"macMode": [
"HMAC-SHA2-256"
],
"supportedLengths": [{
"min": 8,
"max": 4096,
"increment": 8
}],
"fixedDataOrder": [
"before fixed data"
],
"counterLength": [
32
]
}]
}, {
"algorithm": "ACVP-AES-XTS",
"revision": "1.0",
"direction": [
"encrypt",
"decrypt"
],
"keyLen": [
128,
256
],
"payloadLen": [
1024
],
"tweakMode": [
"number"
]
}, {
"algorithm": "KDA",
"mode": "HKDF",
"revision": "Sp800-56Cr1",
"fixedInfoPattern": "uPartyInfo||vPartyInfo",
"encoding": [
"concatenation"
],
"hmacAlg": [
"SHA2-256"
],
"macSaltMethods": [
"default",
"random"
],
"l": 256,
"z": [256, 384]
}, {
"algorithm": "hmacDRBG",
"revision": "1.0",
"predResistanceEnabled": [false, true],
"reseedImplemented": true,
"capabilities": [{
"mode": "SHA2-256",
"derFuncEnabled": false,
"entropyInputLen": [
256
],
"nonceLen": [
128
],
"persoStringLen": [
256
],
"additionalInputLen": [
256
],
"returnedBitsLen": 256
}]
}, {
"algorithm": "ACVP-AES-CBC-CS3",
"revision": "1.0",
"payloadLen": [{
"min": 128,
"max": 2048,
"increment": 8
}],
"direction": [
"encrypt",
"decrypt"
],
"keyLen": [
128,
256
]
}, {
"algorithm": "PBKDF",
"revision":"1.0",
"capabilities": [{
"iterationCount":[{
"min":1,
"max":10000,
"increment":1
}],
"keyLen": [{
"min":112,
"max":4096,
"increment":8
}],
"passwordLen":[{
"min":8,
"max":64,
"increment":1
}],
"saltLen":[{
"min":128,
"max":512,
"increment":8
}],
"hmacAlg":[
"SHA2-224",
"SHA2-256",
"SHA2-384",
"SHA2-512",
"SHA2-512/224",
"SHA2-512/256",
"SHA3-224",
"SHA3-256",
"SHA3-384",
"SHA3-512"
]
}]
}, {
"algorithm": "EDDSA",
"mode": "keyVer",
"revision": "1.0",
"curve": ["ED-25519"]
}, {
"algorithm": "EDDSA",
"mode": "sigVer",
"revision": "1.0",
"pure": true,
"preHash": true,
"curve": ["ED-25519"]
}, {
"algorithm": "SHAKE-128",
"inBit": false,
"outBit": false,
"inEmpty": false,
"outputLen": [{
"min": 128,
"max": 4096,
"increment": 8
}],
"revision": "1.0"
}, {
"algorithm": "SHAKE-256",
"inBit": false,
"outBit": false,
"inEmpty": false,
"outputLen": [{
"min": 128,
"max": 4096,
"increment": 8
}],
"revision": "1.0"
}
]`)); err != nil {
return err
}
return flush(nil)
}
func kdfCounter(args [][]byte) error {
if len(args) != 5 {
return fmt.Errorf("KDF received %d args", len(args))
}
outputBytes32, prf, counterLocation, key, counterBits32 := args[0], args[1], args[2], args[3], args[4]
outputBytes := binary.LittleEndian.Uint32(outputBytes32)
counterBits := binary.LittleEndian.Uint32(counterBits32)
if !bytes.Equal(prf, []byte("HMAC-SHA2-256")) {
return fmt.Errorf("KDF received unsupported PRF %q", string(prf))
}
if !bytes.Equal(counterLocation, []byte("before fixed data")) {
return fmt.Errorf("KDF received unsupported counter location %q", counterLocation)
}
if counterBits != 32 {
return fmt.Errorf("KDF received unsupported counter length %d", counterBits)
}
if len(key) == 0 {
key = make([]byte, 32)
rand.Reader.Read(key)
}
// See https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-108.pdf section 5.1
if outputBytes+31 < outputBytes {
return fmt.Errorf("KDF received excessive output length %d", outputBytes)
}
n := (outputBytes + 31) / 32
result := make([]byte, 0, 32*n)
mac := hmac.New(sha256.New, key)
var input [4 + 8]byte
var digest []byte
rand.Reader.Read(input[4:])
for i := uint32(1); i <= n; i++ {
mac.Reset()
binary.BigEndian.PutUint32(input[:4], i)
mac.Write(input[:])
digest = mac.Sum(digest[:0])
result = append(result, digest...)
}
return reply(key, input[4:], result[:outputBytes])
}
func reply(responses ...[]byte) error {
if len(responses) > maxArgs {
return fmt.Errorf("%d responses is too many", len(responses))
}
var lengths [4 * (1 + maxArgs)]byte
binary.LittleEndian.PutUint32(lengths[:4], uint32(len(responses)))
for i, response := range responses {
binary.LittleEndian.PutUint32(lengths[4*(i+1):4*(i+2)], uint32(len(response)))
}
lengthsLength := (1 + len(responses)) * 4
if n, err := output.Write(lengths[:lengthsLength]); n != lengthsLength || err != nil {
return fmt.Errorf("write failed: %s", err)
}
for _, response := range responses {
if n, err := output.Write(response); n != len(response) || err != nil {
return fmt.Errorf("write failed: %s", err)
}
}
return nil
}
func xtsEncrypt(args [][]byte) error {
return doXTS(args, false)
}
func xtsDecrypt(args [][]byte) error {
return doXTS(args, true)
}
func doXTS(args [][]byte, decrypt bool) error {
if len(args) != 3 {
return fmt.Errorf("XTS received %d args, wanted 3", len(args))
}
key := args[0]
msg := args[1]
tweak := args[2]
if len(msg)%16 != 0 {
return fmt.Errorf("XTS received %d-byte msg, need multiple of 16", len(msg))
}
if len(tweak) != 16 {
return fmt.Errorf("XTS received %d-byte tweak, wanted 16", len(tweak))
}
var zeros [8]byte
if !bytes.Equal(tweak[8:], zeros[:]) {
return errors.New("XTS received tweak with invalid structure. Ensure that configuration specifies a 'number' tweak")
}
sectorNum := binary.LittleEndian.Uint64(tweak[:8])
c, err := xts.NewCipher(aes.NewCipher, key)
if err != nil {
return err
}
if decrypt {
c.Decrypt(msg, msg, sectorNum)
} else {
c.Encrypt(msg, msg, sectorNum)
}
return reply(msg)
}
func hkdfMAC(args [][]byte) error {
if len(args) != 4 {
return fmt.Errorf("HKDF received %d args, wanted 4", len(args))
}
key := args[0]
salt := args[1]
info := args[2]
lengthBytes := args[3]
if len(lengthBytes) != 4 {
return fmt.Errorf("uint32 length was %d bytes long", len(lengthBytes))
}
length := binary.LittleEndian.Uint32(lengthBytes)
mac := hkdf.New(sha256.New, key, salt, info)
ret := make([]byte, length)
mac.Read(ret)
return reply(ret)
}
func hmacDRBGReseed(args [][]byte) error {
if len(args) != 8 {
return fmt.Errorf("hmacDRBG received %d args, wanted 8", len(args))
}
outLenBytes, entropy, personalisation, reseedAdditionalData, reseedEntropy, additionalData1, additionalData2, nonce := args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]
if len(outLenBytes) != 4 {
return fmt.Errorf("uint32 length was %d bytes long", len(outLenBytes))
}
outLen := binary.LittleEndian.Uint32(outLenBytes)
out := make([]byte, outLen)
drbg := NewHMACDRBG(entropy, nonce, personalisation)
drbg.Reseed(reseedEntropy, reseedAdditionalData)
drbg.Generate(out, additionalData1)
drbg.Generate(out, additionalData2)
return reply(out)
}
func hmacDRBGPredictionResistance(args [][]byte) error {
if len(args) != 8 {
return fmt.Errorf("hmacDRBG received %d args, wanted 8", len(args))
}
outLenBytes, entropy, personalisation, additionalData1, entropy1, additionalData2, entropy2, nonce := args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]
if len(outLenBytes) != 4 {
return fmt.Errorf("uint32 length was %d bytes long", len(outLenBytes))
}
outLen := binary.LittleEndian.Uint32(outLenBytes)
out := make([]byte, outLen)
drbg := NewHMACDRBG(entropy, nonce, personalisation)
drbg.Reseed(entropy1, additionalData1)
drbg.Generate(out, nil)
drbg.Reseed(entropy2, additionalData2)
drbg.Generate(out, nil)
return reply(out)
}
func swapFinalTwoAESBlocks(d []byte) {
var blockNMinus1 [aes.BlockSize]byte
copy(blockNMinus1[:], d[len(d)-2*aes.BlockSize:])
copy(d[len(d)-2*aes.BlockSize:], d[len(d)-aes.BlockSize:])
copy(d[len(d)-aes.BlockSize:], blockNMinus1[:])
}
func roundUp(n, m int) int {
return n + (m-(n%m))%m
}
func doCTSEncrypt(key, origPlaintext, iv []byte) []byte {
// https://nvlpubs.nist.gov/nistpubs/legacy/sp/nistspecialpublication800-38a-add.pdf
if len(origPlaintext) < aes.BlockSize {
panic("input too small")
}
plaintext := make([]byte, roundUp(len(origPlaintext), aes.BlockSize))
copy(plaintext, origPlaintext)
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
cbcEncryptor := cipher.NewCBCEncrypter(block, iv)
cbcEncryptor.CryptBlocks(plaintext, plaintext)
ciphertext := plaintext
if len(origPlaintext) > aes.BlockSize {
swapFinalTwoAESBlocks(ciphertext)
if len(origPlaintext)%16 != 0 {
// Truncate the ciphertext
ciphertext = ciphertext[:len(ciphertext)-aes.BlockSize+(len(origPlaintext)%aes.BlockSize)]
}
}
if len(ciphertext) != len(origPlaintext) {
panic("internal error")
}
return ciphertext
}
func doCTSDecrypt(key, origCiphertext, iv []byte) []byte {
if len(origCiphertext) < aes.BlockSize {
panic("input too small")
}
ciphertext := make([]byte, roundUp(len(origCiphertext), aes.BlockSize))
copy(ciphertext, origCiphertext)
if len(ciphertext) > aes.BlockSize {
swapFinalTwoAESBlocks(ciphertext)
}
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
cbcDecrypter := cipher.NewCBCDecrypter(block, iv)
var plaintext []byte
if overhang := len(origCiphertext) % aes.BlockSize; overhang == 0 {
cbcDecrypter.CryptBlocks(ciphertext, ciphertext)
plaintext = ciphertext
} else {
ciphertext, finalBlock := ciphertext[:len(ciphertext)-aes.BlockSize], ciphertext[len(ciphertext)-aes.BlockSize:]
var plaintextFinalBlock [aes.BlockSize]byte
block.Decrypt(plaintextFinalBlock[:], finalBlock)
copy(ciphertext[len(ciphertext)-aes.BlockSize+overhang:], plaintextFinalBlock[overhang:])
plaintext = make([]byte, len(origCiphertext))
cbcDecrypter.CryptBlocks(plaintext, ciphertext)
for i := 0; i < overhang; i++ {
plaintextFinalBlock[i] ^= ciphertext[len(ciphertext)-aes.BlockSize+i]
}
copy(plaintext[len(ciphertext):], plaintextFinalBlock[:overhang])
}
return plaintext
}
func ctsEncrypt(args [][]byte) error {
if len(args) != 4 {
return fmt.Errorf("ctsEncrypt received %d args, wanted 4", len(args))
}
key, plaintext, iv, numIterations32 := args[0], args[1], args[2], args[3]
if len(numIterations32) != 4 || binary.LittleEndian.Uint32(numIterations32) != 1 {
return errors.New("only a single iteration supported for ctsEncrypt")
}
if len(plaintext) < aes.BlockSize {
return fmt.Errorf("ctsEncrypt plaintext too short: %d bytes", len(plaintext))
}
return reply(doCTSEncrypt(key, plaintext, iv))
}
func ctsDecrypt(args [][]byte) error {
if len(args) != 4 {
return fmt.Errorf("ctsDecrypt received %d args, wanted 4", len(args))
}
key, ciphertext, iv, numIterations32 := args[0], args[1], args[2], args[3]
if len(numIterations32) != 4 || binary.LittleEndian.Uint32(numIterations32) != 1 {
return errors.New("only a single iteration supported for ctsDecrypt")
}
if len(ciphertext) < aes.BlockSize {
return errors.New("ctsDecrypt ciphertext too short")
}
return reply(doCTSDecrypt(key, ciphertext, iv))
}
func pbkdf(args [][]byte) error {
if len(args) != 5 {
return fmt.Errorf("pbkdf received %d args, wanted 5", len(args))
}
hmacName := args[0]
var h func() hash.Hash
switch string(hmacName) {
case "SHA2-224":
h = sha256.New224
case "SHA2-256":
h = sha256.New
case "SHA2-384":
h = sha512.New384
case "SHA2-512":
h = sha512.New
case "SHA2-512/224":
h = sha512.New512_224
case "SHA2-512/256":
h = sha512.New512_256
case "SHA3-224":
h = sha3.New224
case "SHA3-256":
h = sha3.New256
case "SHA3-384":
h = sha3.New384
case "SHA3-512":
h = sha3.New512
default:
return fmt.Errorf("pbkdf unknown HMAC algorithm: %q", hmacName)
}
keyLen := binary.LittleEndian.Uint32(args[1]) / 8
salt, password := args[2], args[3]
iterationCount := binary.LittleEndian.Uint32(args[4])
derivedKey := pbkdf2.Key(password, salt, int(iterationCount), int(keyLen), h)
return reply(derivedKey)
}
func eddsaKeyGen(args [][]byte) error {
if string(args[0]) != "ED-25519" {
return fmt.Errorf("unsupported EDDSA curve: %q", args[0])
}
pk, sk, err := ed25519.GenerateKey(nil)
if err != nil {
return fmt.Errorf("generating EDDSA keypair: %w", err)
}
// EDDSA/keyGen/AFT responses are d & q, described[0] as:
// d The encoded private key point
// q The encoded public key point
//
// Contrary to the description of a "point", d is the private key
// seed bytes per FIPS.186-5[1] A.2.3.
//
// [0]: https://pages.nist.gov/ACVP/draft-celi-acvp-eddsa.html#section-9.1
// [1]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf
return reply(sk.Seed(), pk)
}
func eddsaKeyVer(args [][]byte) error {
if string(args[0]) != "ED-25519" {
return fmt.Errorf("unsupported EDDSA curve: %q", args[0])
}
if len(args[1]) != ed25519.PublicKeySize {
return reply([]byte{0})
}
// Verify the point is on the curve. The higher-level ed25519 API does
// this at signature verification time so we have to use the lower-level
// edwards25519 package to do it here in absence of a signature to verify.
if _, err := new(edwards25519.Point).SetBytes(args[1]); err != nil {
return reply([]byte{0})
}
return reply([]byte{1})
}
func eddsaSigGen(args [][]byte) error {
if string(args[0]) != "ED-25519" {
return fmt.Errorf("unsupported EDDSA curve: %q", args[0])
}
sk := ed25519.NewKeyFromSeed(args[1])
msg := args[2]
prehash := args[3]
context := string(args[4])
var opts ed25519.Options
if prehash[0] == 1 {
opts.Hash = crypto.SHA512
h := sha512.New()
h.Write(msg)
msg = h.Sum(nil)
// With ed25519 the context is only specified for sigGen tests when using prehashing.
// See https://pages.nist.gov/ACVP/draft-celi-acvp-eddsa.html#section-8.6
opts.Context = context
}
sig, err := sk.Sign(nil, msg, &opts)
if err != nil {
return fmt.Errorf("error signing message: %w", err)
}
return reply(sig)
}
func eddsaSigVer(args [][]byte) error {
if string(args[0]) != "ED-25519" {
return fmt.Errorf("unsupported EDDSA curve: %q", args[0])
}
msg := args[1]
pk := ed25519.PublicKey(args[2])
sig := args[3]
prehash := args[4]
var opts ed25519.Options
if prehash[0] == 1 {
opts.Hash = crypto.SHA512
h := sha512.New()
h.Write(msg)
msg = h.Sum(nil)
// Context is only specified for sigGen, not sigVer.
// See https://pages.nist.gov/ACVP/draft-celi-acvp-eddsa.html#section-8.6
}
if err := ed25519.VerifyWithOptions(pk, msg, sig, &opts); err != nil {
return reply([]byte{0})
}
return reply([]byte{1})
}
func shakeAftVot(digestFn func() sha3.ShakeHash) func([][]byte) error {
return func(args [][]byte) error {
if len(args) != 2 {
return fmt.Errorf("shakeAftVot received %d args, wanted 2", len(args))
}
msg := args[0]
outLenBytes := binary.LittleEndian.Uint32(args[1])
h := digestFn()
h.Write(msg)
digest := make([]byte, outLenBytes)
h.Read(digest)
return reply(digest)
}
}
func shakeMct(digestFn func() sha3.ShakeHash) func([][]byte) error {
return func(args [][]byte) error {
if len(args) != 4 {
return fmt.Errorf("shakeMct received %d args, wanted 4", len(args))
}
md := args[0]
minOutBytes := binary.LittleEndian.Uint32(args[1])
maxOutBytes := binary.LittleEndian.Uint32(args[2])
outputLenBytes := binary.LittleEndian.Uint32(args[3])
if outputLenBytes < 2 {
return fmt.Errorf("invalid output length: %d", outputLenBytes)
}
if maxOutBytes < minOutBytes {
return fmt.Errorf("invalid maxOutBytes and minOutBytes: %d, %d", maxOutBytes, minOutBytes)
}
rangeBytes := maxOutBytes - minOutBytes + 1
for i := 0; i < 1000; i++ {
// "The MSG[i] input to SHAKE MUST always contain at least 128 bits. If this is not the case
// as the previous digest was too short, append empty bits to the rightmost side of the digest."
boundary := min(len(md), 16)
msg := make([]byte, 16)
copy(msg, md[:boundary])
// MD[i] = SHAKE(MSG[i], OutputLen * 8)
h := digestFn()
h.Write(msg)
digest := make([]byte, outputLenBytes)
h.Read(digest)
md = digest
// RightmostOutputBits = 16 rightmost bits of MD[i] as an integer
// OutputLen = minOutBytes + (RightmostOutputBits % Range)
rightmostOutput := uint32(md[outputLenBytes-2])<<8 | uint32(md[outputLenBytes-1])
outputLenBytes = minOutBytes + (rightmostOutput % rangeBytes)
}
encodedOutputLenBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(encodedOutputLenBytes, outputLenBytes)
return reply(md, encodedOutputLenBytes)
}
}
const (
maxArgs = 9
maxArgLength = 1 << 20
maxNameLength = 30
)
func main() {
if err := do(); err != nil {
fmt.Fprintf(os.Stderr, "%s.\n", err)
os.Exit(1)
}
}
func do() error {
// In order to exercise pipelining, all output is buffered until a "flush".
outputBuffer = new(bytes.Buffer)
output = outputBuffer
var nums [4 * (1 + maxArgs)]byte
var argLengths [maxArgs]uint32
var args [maxArgs][]byte
var argsData []byte
for {
if _, err := io.ReadFull(os.Stdin, nums[:8]); err != nil {
return err
}
numArgs := binary.LittleEndian.Uint32(nums[:4])
if numArgs == 0 {
return errors.New("Invalid, zero-argument operation requested")
} else if numArgs > maxArgs {
return fmt.Errorf("Operation requested with %d args, but %d is the limit", numArgs, maxArgs)
}
if numArgs > 1 {
if _, err := io.ReadFull(os.Stdin, nums[8:4+4*numArgs]); err != nil {
return err
}
}
input := nums[4:]
var need uint64
for i := uint32(0); i < numArgs; i++ {
argLength := binary.LittleEndian.Uint32(input[:4])
if i == 0 && argLength > maxNameLength {
return fmt.Errorf("Operation with name of length %d exceeded limit of %d", argLength, maxNameLength)
} else if argLength > maxArgLength {
return fmt.Errorf("Operation with argument of length %d exceeded limit of %d", argLength, maxArgLength)
}
need += uint64(argLength)
argLengths[i] = argLength
input = input[4:]
}
if need > uint64(cap(argsData)) {
argsData = make([]byte, need)
} else {
argsData = argsData[:need]
}
if _, err := io.ReadFull(os.Stdin, argsData); err != nil {
return err
}
input = argsData
for i := uint32(0); i < numArgs; i++ {
args[i] = input[:argLengths[i]]
input = input[argLengths[i]:]
}
name := string(args[0])
if handler, ok := handlers[name]; !ok {
return fmt.Errorf("unknown operation %q", name)
} else {
if err := handler(args[1:numArgs]); err != nil {
return err
}
}
}
}