1
2
3
4
5 package fipstest
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 import (
22 "bufio"
23 "bytes"
24 "crypto/internal/cryptotest"
25 "crypto/internal/fips140"
26 "crypto/internal/fips140/ecdsa"
27 "crypto/internal/fips140/hmac"
28 "crypto/internal/fips140/mlkem"
29 "crypto/internal/fips140/pbkdf2"
30 "crypto/internal/fips140/sha256"
31 "crypto/internal/fips140/sha3"
32 "crypto/internal/fips140/sha512"
33 _ "embed"
34 "encoding/binary"
35 "errors"
36 "fmt"
37 "internal/testenv"
38 "io"
39 "os"
40 "path/filepath"
41 "strings"
42 "testing"
43 )
44
45 func TestMain(m *testing.M) {
46 if os.Getenv("ACVP_WRAPPER") == "1" {
47 wrapperMain()
48 } else {
49 os.Exit(m.Run())
50 }
51 }
52
53 func wrapperMain() {
54 if err := processingLoop(bufio.NewReader(os.Stdin), os.Stdout); err != nil {
55 fmt.Fprintf(os.Stderr, "processing error: %v\n", err)
56 os.Exit(1)
57 }
58 }
59
60 type request struct {
61 name string
62 args [][]byte
63 }
64
65 type commandHandler func([][]byte) ([][]byte, error)
66
67 type command struct {
68
69 requiredArgs int
70 handler commandHandler
71 }
72
73 var (
74
75
76
77
78
79
80
81
82
83
84
85 capabilitiesJson []byte
86
87
88
89
90 commands = map[string]command{
91 "getConfig": cmdGetConfig(),
92
93 "SHA2-224": cmdHashAft(sha256.New224()),
94 "SHA2-224/MCT": cmdHashMct(sha256.New224()),
95 "SHA2-256": cmdHashAft(sha256.New()),
96 "SHA2-256/MCT": cmdHashMct(sha256.New()),
97 "SHA2-384": cmdHashAft(sha512.New384()),
98 "SHA2-384/MCT": cmdHashMct(sha512.New384()),
99 "SHA2-512": cmdHashAft(sha512.New()),
100 "SHA2-512/MCT": cmdHashMct(sha512.New()),
101 "SHA2-512/224": cmdHashAft(sha512.New512_224()),
102 "SHA2-512/224/MCT": cmdHashMct(sha512.New512_224()),
103 "SHA2-512/256": cmdHashAft(sha512.New512_256()),
104 "SHA2-512/256/MCT": cmdHashMct(sha512.New512_256()),
105
106 "SHA3-256": cmdHashAft(sha3.New256()),
107 "SHA3-256/MCT": cmdSha3Mct(sha3.New256()),
108 "SHA3-224": cmdHashAft(sha3.New224()),
109 "SHA3-224/MCT": cmdSha3Mct(sha3.New224()),
110 "SHA3-384": cmdHashAft(sha3.New384()),
111 "SHA3-384/MCT": cmdSha3Mct(sha3.New384()),
112 "SHA3-512": cmdHashAft(sha3.New512()),
113 "SHA3-512/MCT": cmdSha3Mct(sha3.New512()),
114
115 "HMAC-SHA2-224": cmdHmacAft(func() fips140.Hash { return sha256.New224() }),
116 "HMAC-SHA2-256": cmdHmacAft(func() fips140.Hash { return sha256.New() }),
117 "HMAC-SHA2-384": cmdHmacAft(func() fips140.Hash { return sha512.New384() }),
118 "HMAC-SHA2-512": cmdHmacAft(func() fips140.Hash { return sha512.New() }),
119 "HMAC-SHA2-512/224": cmdHmacAft(func() fips140.Hash { return sha512.New512_224() }),
120 "HMAC-SHA2-512/256": cmdHmacAft(func() fips140.Hash { return sha512.New512_256() }),
121 "HMAC-SHA3-224": cmdHmacAft(func() fips140.Hash { return sha3.New224() }),
122 "HMAC-SHA3-256": cmdHmacAft(func() fips140.Hash { return sha3.New256() }),
123 "HMAC-SHA3-384": cmdHmacAft(func() fips140.Hash { return sha3.New384() }),
124 "HMAC-SHA3-512": cmdHmacAft(func() fips140.Hash { return sha3.New512() }),
125
126 "PBKDF": cmdPbkdf(),
127
128 "ML-KEM-768/keyGen": cmdMlKem768KeyGenAft(),
129 "ML-KEM-768/encap": cmdMlKem768EncapAft(),
130 "ML-KEM-768/decap": cmdMlKem768DecapAft(),
131 "ML-KEM-1024/keyGen": cmdMlKem1024KeyGenAft(),
132 "ML-KEM-1024/encap": cmdMlKem1024EncapAft(),
133 "ML-KEM-1024/decap": cmdMlKem1024DecapAft(),
134
135 "hmacDRBG/SHA2-224": cmdHmacDrbgAft(func() fips140.Hash { return sha256.New224() }),
136 "hmacDRBG/SHA2-256": cmdHmacDrbgAft(func() fips140.Hash { return sha256.New() }),
137 "hmacDRBG/SHA2-384": cmdHmacDrbgAft(func() fips140.Hash { return sha512.New384() }),
138 "hmacDRBG/SHA2-512": cmdHmacDrbgAft(func() fips140.Hash { return sha512.New() }),
139 "hmacDRBG/SHA2-512/224": cmdHmacDrbgAft(func() fips140.Hash { return sha512.New512_224() }),
140 "hmacDRBG/SHA2-512/256": cmdHmacDrbgAft(func() fips140.Hash { return sha512.New512_256() }),
141 "hmacDRBG/SHA3-224": cmdHmacDrbgAft(func() fips140.Hash { return sha3.New224() }),
142 "hmacDRBG/SHA3-256": cmdHmacDrbgAft(func() fips140.Hash { return sha3.New256() }),
143 "hmacDRBG/SHA3-384": cmdHmacDrbgAft(func() fips140.Hash { return sha3.New384() }),
144 "hmacDRBG/SHA3-512": cmdHmacDrbgAft(func() fips140.Hash { return sha3.New512() }),
145 }
146 )
147
148 func processingLoop(reader io.Reader, writer io.Writer) error {
149
150
151
152 for {
153 req, err := readRequest(reader)
154 if errors.Is(err, io.EOF) {
155 break
156 } else if err != nil {
157 return fmt.Errorf("reading request: %w", err)
158 }
159
160 cmd, exists := commands[req.name]
161 if !exists {
162 return fmt.Errorf("unknown command: %q", req.name)
163 }
164
165 if gotArgs := len(req.args); gotArgs != cmd.requiredArgs {
166 return fmt.Errorf("command %q expected %d args, got %d", req.name, cmd.requiredArgs, gotArgs)
167 }
168
169 response, err := cmd.handler(req.args)
170 if err != nil {
171 return fmt.Errorf("command %q failed: %w", req.name, err)
172 }
173
174 if err = writeResponse(writer, response); err != nil {
175 return fmt.Errorf("command %q response failed: %w", req.name, err)
176 }
177 }
178
179 return nil
180 }
181
182 func readRequest(reader io.Reader) (*request, error) {
183
184
185
186
187
188
189 var numArgs uint32
190 if err := binary.Read(reader, binary.LittleEndian, &numArgs); err != nil {
191 return nil, err
192 }
193 if numArgs == 0 {
194 return nil, errors.New("invalid request: zero args")
195 }
196
197 args, err := readArgs(reader, numArgs)
198 if err != nil {
199 return nil, err
200 }
201
202 return &request{
203 name: string(args[0]),
204 args: args[1:],
205 }, nil
206 }
207
208 func readArgs(reader io.Reader, requiredArgs uint32) ([][]byte, error) {
209 argLengths := make([]uint32, requiredArgs)
210 args := make([][]byte, requiredArgs)
211
212 for i := range argLengths {
213 if err := binary.Read(reader, binary.LittleEndian, &argLengths[i]); err != nil {
214 return nil, fmt.Errorf("invalid request: failed to read %d-th arg len: %w", i, err)
215 }
216 }
217
218 for i, length := range argLengths {
219 buf := make([]byte, length)
220 if _, err := io.ReadFull(reader, buf); err != nil {
221 return nil, fmt.Errorf("invalid request: failed to read %d-th arg data: %w", i, err)
222 }
223 args[i] = buf
224 }
225
226 return args, nil
227 }
228
229 func writeResponse(writer io.Writer, args [][]byte) error {
230
231
232
233 numArgs := uint32(len(args))
234 if err := binary.Write(writer, binary.LittleEndian, numArgs); err != nil {
235 return fmt.Errorf("writing arg count: %w", err)
236 }
237
238 for i, arg := range args {
239 if err := binary.Write(writer, binary.LittleEndian, uint32(len(arg))); err != nil {
240 return fmt.Errorf("writing %d-th arg length: %w", i, err)
241 }
242 }
243
244 for i, b := range args {
245 if _, err := writer.Write(b); err != nil {
246 return fmt.Errorf("writing %d-th arg data: %w", i, err)
247 }
248 }
249
250 return nil
251 }
252
253
254
255
256 func cmdGetConfig() command {
257 return command{
258 handler: func(args [][]byte) ([][]byte, error) {
259 return [][]byte{capabilitiesJson}, nil
260 },
261 }
262 }
263
264
265
266
267
268
269
270
271 func cmdHashAft(h fips140.Hash) command {
272 return command{
273 requiredArgs: 1,
274 handler: func(args [][]byte) ([][]byte, error) {
275 h.Reset()
276 h.Write(args[0])
277 digest := make([]byte, 0, h.Size())
278 digest = h.Sum(digest)
279
280 return [][]byte{digest}, nil
281 },
282 }
283 }
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299 func cmdHashMct(h fips140.Hash) command {
300 return command{
301 requiredArgs: 1,
302 handler: func(args [][]byte) ([][]byte, error) {
303 hSize := h.Size()
304 seed := args[0]
305
306 if seedLen := len(seed); seedLen != hSize {
307 return nil, fmt.Errorf("invalid seed size: expected %d got %d", hSize, seedLen)
308 }
309
310 digest := make([]byte, 0, hSize)
311 buf := make([]byte, 0, 3*hSize)
312 buf = append(buf, seed...)
313 buf = append(buf, seed...)
314 buf = append(buf, seed...)
315
316 for i := 0; i < 1000; i++ {
317 h.Reset()
318 h.Write(buf)
319 digest = h.Sum(digest[:0])
320
321 copy(buf, buf[hSize:])
322 copy(buf[2*hSize:], digest)
323 }
324
325 return [][]byte{buf[hSize*2:]}, nil
326 },
327 }
328 }
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343 func cmdSha3Mct(h fips140.Hash) command {
344 return command{
345 requiredArgs: 1,
346 handler: func(args [][]byte) ([][]byte, error) {
347 seed := args[0]
348 md := make([][]byte, 1001)
349 md[0] = seed
350
351 for i := 1; i <= 1000; i++ {
352 h.Reset()
353 h.Write(md[i-1])
354 md[i] = h.Sum(nil)
355 }
356
357 return [][]byte{md[1000]}, nil
358 },
359 }
360 }
361
362 func cmdHmacAft(h func() fips140.Hash) command {
363 return command{
364 requiredArgs: 2,
365 handler: func(args [][]byte) ([][]byte, error) {
366 msg := args[0]
367 key := args[1]
368 mac := hmac.New(h, key)
369 mac.Write(msg)
370 return [][]byte{mac.Sum(nil)}, nil
371 },
372 }
373 }
374
375 func cmdPbkdf() command {
376 return command{
377
378 requiredArgs: 5,
379 handler: func(args [][]byte) ([][]byte, error) {
380 h, err := lookupHash(string(args[0]))
381 if err != nil {
382 return nil, fmt.Errorf("PBKDF2 failed: %w", err)
383 }
384
385 keyLen := binary.LittleEndian.Uint32(args[1]) / 8
386 salt := args[2]
387 password := args[3]
388 iterationCount := binary.LittleEndian.Uint32(args[4])
389
390 derivedKey, err := pbkdf2.Key(h, string(password), salt, int(iterationCount), int(keyLen))
391 if err != nil {
392 return nil, fmt.Errorf("PBKDF2 failed: %w", err)
393 }
394
395 return [][]byte{derivedKey}, nil
396 },
397 }
398 }
399
400 func lookupHash(name string) (func() fips140.Hash, error) {
401 var h func() fips140.Hash
402
403 switch name {
404 case "SHA2-224":
405 h = func() fips140.Hash { return sha256.New224() }
406 case "SHA2-256":
407 h = func() fips140.Hash { return sha256.New() }
408 case "SHA2-384":
409 h = func() fips140.Hash { return sha512.New384() }
410 case "SHA2-512":
411 h = func() fips140.Hash { return sha512.New() }
412 case "SHA2-512/224":
413 h = func() fips140.Hash { return sha512.New512_224() }
414 case "SHA2-512/256":
415 h = func() fips140.Hash { return sha512.New512_256() }
416 case "SHA3-224":
417 h = func() fips140.Hash { return sha3.New224() }
418 case "SHA3-256":
419 h = func() fips140.Hash { return sha3.New256() }
420 case "SHA3-384":
421 h = func() fips140.Hash { return sha3.New384() }
422 case "SHA3-512":
423 h = func() fips140.Hash { return sha3.New512() }
424 default:
425 return nil, fmt.Errorf("unknown hash name: %q", name)
426 }
427
428 return h, nil
429 }
430
431 func cmdMlKem768KeyGenAft() command {
432 return command{
433 requiredArgs: 1,
434 handler: func(args [][]byte) ([][]byte, error) {
435 seed := args[0]
436
437 dk, err := mlkem.NewDecapsulationKey768(seed)
438 if err != nil {
439 return nil, fmt.Errorf("generating ML-KEM 768 decapsulation key: %w", err)
440 }
441
442
443 return [][]byte{dk.EncapsulationKey().Bytes(), mlkem.TestingOnlyExpandedBytes768(dk)}, nil
444 },
445 }
446 }
447
448 func cmdMlKem768EncapAft() command {
449 return command{
450 requiredArgs: 2,
451 handler: func(args [][]byte) ([][]byte, error) {
452 pk := args[0]
453 entropy := args[1]
454
455 ek, err := mlkem.NewEncapsulationKey768(pk)
456 if err != nil {
457 return nil, fmt.Errorf("generating ML-KEM 768 encapsulation key: %w", err)
458 }
459
460 if len(entropy) != 32 {
461 return nil, fmt.Errorf("wrong entropy length: got %d, want 32", len(entropy))
462 }
463
464 sharedKey, ct := ek.EncapsulateInternal((*[32]byte)(entropy[:32]))
465
466 return [][]byte{ct, sharedKey}, nil
467 },
468 }
469 }
470
471 func cmdMlKem768DecapAft() command {
472 return command{
473 requiredArgs: 2,
474 handler: func(args [][]byte) ([][]byte, error) {
475 pk := args[0]
476 ct := args[1]
477
478 dk, err := mlkem.TestingOnlyNewDecapsulationKey768(pk)
479 if err != nil {
480 return nil, fmt.Errorf("generating ML-KEM 768 decapsulation key: %w", err)
481 }
482
483 sharedKey, err := dk.Decapsulate(ct)
484 if err != nil {
485 return nil, fmt.Errorf("decapsulating ML-KEM 768 ciphertext: %w", err)
486 }
487
488 return [][]byte{sharedKey}, nil
489 },
490 }
491 }
492
493 func cmdMlKem1024KeyGenAft() command {
494 return command{
495 requiredArgs: 1,
496 handler: func(args [][]byte) ([][]byte, error) {
497 seed := args[0]
498
499 dk, err := mlkem.NewDecapsulationKey1024(seed)
500 if err != nil {
501 return nil, fmt.Errorf("generating ML-KEM 1024 decapsulation key: %w", err)
502 }
503
504
505 return [][]byte{dk.EncapsulationKey().Bytes(), mlkem.TestingOnlyExpandedBytes1024(dk)}, nil
506 },
507 }
508 }
509
510 func cmdMlKem1024EncapAft() command {
511 return command{
512 requiredArgs: 2,
513 handler: func(args [][]byte) ([][]byte, error) {
514 pk := args[0]
515 entropy := args[1]
516
517 ek, err := mlkem.NewEncapsulationKey1024(pk)
518 if err != nil {
519 return nil, fmt.Errorf("generating ML-KEM 1024 encapsulation key: %w", err)
520 }
521
522 if len(entropy) != 32 {
523 return nil, fmt.Errorf("wrong entropy length: got %d, want 32", len(entropy))
524 }
525
526 sharedKey, ct := ek.EncapsulateInternal((*[32]byte)(entropy[:32]))
527
528 return [][]byte{ct, sharedKey}, nil
529 },
530 }
531 }
532
533 func cmdMlKem1024DecapAft() command {
534 return command{
535 requiredArgs: 2,
536 handler: func(args [][]byte) ([][]byte, error) {
537 pk := args[0]
538 ct := args[1]
539
540 dk, err := mlkem.TestingOnlyNewDecapsulationKey1024(pk)
541 if err != nil {
542 return nil, fmt.Errorf("generating ML-KEM 1024 decapsulation key: %w", err)
543 }
544
545 sharedKey, err := dk.Decapsulate(ct)
546 if err != nil {
547 return nil, fmt.Errorf("decapsulating ML-KEM 1024 ciphertext: %w", err)
548 }
549
550 return [][]byte{sharedKey}, nil
551 },
552 }
553 }
554
555 func cmdHmacDrbgAft(h func() fips140.Hash) command {
556 return command{
557 requiredArgs: 6,
558 handler: func(args [][]byte) ([][]byte, error) {
559 outLen := binary.LittleEndian.Uint32(args[0])
560 entropy := args[1]
561 personalization := args[2]
562 ad1 := args[3]
563 ad2 := args[4]
564 nonce := args[5]
565
566
567 if len(ad1) != 0 || len(ad2) != 0 {
568 return nil, errors.New("additional data not supported")
569 }
570
571
572
573
574
575
576
577
578 out := make([]byte, outLen)
579 drbg := ecdsa.TestingOnlyNewDRBG(h, entropy, nonce, personalization)
580 drbg.Generate(out)
581 drbg.Generate(out)
582
583 return [][]byte{out}, nil
584 },
585 }
586 }
587
588 func TestACVP(t *testing.T) {
589 testenv.SkipIfShortAndSlow(t)
590
591 const (
592 bsslModule = "boringssl.googlesource.com/boringssl.git"
593 bsslVersion = "v0.0.0-20250108043213-d3f61eeacbf7"
594 goAcvpModule = "github.com/cpu/go-acvp"
595 goAcvpVersion = "v0.0.0-20250102201911-6839fc40f9f8"
596 )
597
598
599
600
601
602
603 if _, err := os.Stat("acvp_test.config.json"); err != nil {
604 t.Fatalf("failed to stat config file: %s", err)
605 }
606
607
608 bsslDir := cryptotest.FetchModule(t, bsslModule, bsslVersion)
609
610 t.Log("building acvptool")
611
612
613 toolPath := filepath.Join(t.TempDir(), "acvptool.exe")
614 goTool := testenv.GoToolPath(t)
615 cmd := testenv.Command(t, goTool,
616 "build",
617 "-o", toolPath,
618 "./util/fipstools/acvp/acvptool")
619 cmd.Dir = bsslDir
620 out := &strings.Builder{}
621 cmd.Stderr = out
622 if err := cmd.Run(); err != nil {
623 t.Fatalf("failed to build acvptool: %s\n%s", err, out.String())
624 }
625
626
627 dataDir := cryptotest.FetchModule(t, goAcvpModule, goAcvpVersion)
628
629 cwd, err := os.Getwd()
630 if err != nil {
631 t.Fatalf("failed to fetch cwd: %s", err)
632 }
633 configPath := filepath.Join(cwd, "acvp_test.config.json")
634 t.Logf("running check_expected.go\ncwd: %q\ndata_dir: %q\nconfig: %q\ntool: %q\nmodule-wrapper: %q\n",
635 cwd, dataDir, configPath, toolPath, os.Args[0])
636
637
638
639
640 args := []string{
641 "run",
642 filepath.Join(bsslDir, "util/fipstools/acvp/acvptool/test/check_expected.go"),
643 "-tool",
644 toolPath,
645
646 "-module-wrappers", "go:" + os.Args[0],
647 "-tests", configPath,
648 }
649 cmd = testenv.Command(t, goTool, args...)
650 cmd.Dir = dataDir
651 cmd.Env = append(os.Environ(), "ACVP_WRAPPER=1")
652 output, err := cmd.CombinedOutput()
653 if err != nil {
654 t.Fatalf("failed to run acvp tests: %s\n%s", err, string(output))
655 }
656 t.Log(string(output))
657 }
658
659 func TestTooFewArgs(t *testing.T) {
660 commands["test"] = command{
661 requiredArgs: 1,
662 handler: func(args [][]byte) ([][]byte, error) {
663 if gotArgs := len(args); gotArgs != 1 {
664 return nil, fmt.Errorf("expected 1 args, got %d", gotArgs)
665 }
666 return nil, nil
667 },
668 }
669
670 var output bytes.Buffer
671 err := processingLoop(mockRequest(t, "test", nil), &output)
672 if err == nil {
673 t.Fatalf("expected error, got nil")
674 }
675 expectedErr := "expected 1 args, got 0"
676 if !strings.Contains(err.Error(), expectedErr) {
677 t.Errorf("expected error to contain %q, got %v", expectedErr, err)
678 }
679 }
680
681 func TestTooManyArgs(t *testing.T) {
682 commands["test"] = command{
683 requiredArgs: 1,
684 handler: func(args [][]byte) ([][]byte, error) {
685 if gotArgs := len(args); gotArgs != 1 {
686 return nil, fmt.Errorf("expected 1 args, got %d", gotArgs)
687 }
688 return nil, nil
689 },
690 }
691
692 var output bytes.Buffer
693 err := processingLoop(mockRequest(
694 t, "test", [][]byte{[]byte("one"), []byte("two")}), &output)
695 if err == nil {
696 t.Fatalf("expected error, got nil")
697 }
698 expectedErr := "expected 1 args, got 2"
699 if !strings.Contains(err.Error(), expectedErr) {
700 t.Errorf("expected error to contain %q, got %v", expectedErr, err)
701 }
702 }
703
704 func TestGetConfig(t *testing.T) {
705 var output bytes.Buffer
706 err := processingLoop(mockRequest(t, "getConfig", nil), &output)
707 if err != nil {
708 t.Errorf("unexpected error: %v", err)
709 }
710
711 respArgs := readResponse(t, &output)
712 if len(respArgs) != 1 {
713 t.Fatalf("expected 1 response arg, got %d", len(respArgs))
714 }
715
716 if !bytes.Equal(respArgs[0], capabilitiesJson) {
717 t.Errorf("expected config %q, got %q", string(capabilitiesJson), string(respArgs[0]))
718 }
719 }
720
721 func TestSha2256(t *testing.T) {
722 testMessage := []byte("gophers eat grass")
723 expectedDigest := []byte{
724 188, 142, 10, 214, 48, 236, 72, 143, 70, 216, 223, 205, 219, 69, 53, 29,
725 205, 207, 162, 6, 14, 70, 113, 60, 251, 170, 201, 236, 119, 39, 141, 172,
726 }
727
728 var output bytes.Buffer
729 err := processingLoop(mockRequest(t, "SHA2-256", [][]byte{testMessage}), &output)
730 if err != nil {
731 t.Errorf("unexpected error: %v", err)
732 }
733
734 respArgs := readResponse(t, &output)
735 if len(respArgs) != 1 {
736 t.Fatalf("expected 1 response arg, got %d", len(respArgs))
737 }
738
739 if !bytes.Equal(respArgs[0], expectedDigest) {
740 t.Errorf("expected digest %v, got %v", expectedDigest, respArgs[0])
741 }
742 }
743
744 func mockRequest(t *testing.T, cmd string, args [][]byte) io.Reader {
745 t.Helper()
746
747 msgData := append([][]byte{[]byte(cmd)}, args...)
748
749 var buf bytes.Buffer
750 if err := writeResponse(&buf, msgData); err != nil {
751 t.Fatalf("writeResponse error: %v", err)
752 }
753
754 return &buf
755 }
756
757 func readResponse(t *testing.T, reader io.Reader) [][]byte {
758 var numArgs uint32
759 if err := binary.Read(reader, binary.LittleEndian, &numArgs); err != nil {
760 t.Fatalf("failed to read response args count: %v", err)
761 }
762
763 args, err := readArgs(reader, numArgs)
764 if err != nil {
765 t.Fatalf("failed to read %d response args: %v", numArgs, err)
766 }
767
768 return args
769 }
770
View as plain text