1
2
3
4
5 package fips140only_test
6
7 import (
8 "crypto"
9 "crypto/aes"
10 "crypto/cipher"
11 "crypto/des"
12 "crypto/dsa"
13 "crypto/ecdh"
14 "crypto/ecdsa"
15 "crypto/ed25519"
16 "crypto/elliptic"
17 "crypto/hkdf"
18 "crypto/hmac"
19 "crypto/hpke"
20 "crypto/internal/cryptotest"
21 "crypto/internal/fips140"
22 "crypto/internal/fips140only"
23 "crypto/md5"
24 "crypto/mlkem"
25 "crypto/mlkem/mlkemtest"
26 "crypto/pbkdf2"
27 "crypto/rand"
28 "crypto/rc4"
29 "crypto/rsa"
30 "crypto/sha1"
31 "crypto/sha256"
32 _ "crypto/sha3"
33 _ "crypto/sha512"
34 "crypto/x509"
35 "encoding/pem"
36 "fmt"
37 "internal/godebug"
38 "io"
39 "math/big"
40 "os"
41 "strings"
42 "testing"
43
44 "golang.org/x/crypto/chacha20poly1305"
45 )
46
47 func TestFIPS140Only(t *testing.T) {
48 if !fips140only.Enforced() {
49 cryptotest.RerunWithFIPS140Enforced(t)
50 return
51 }
52 t.Run("cryptocustomrand=0", func(t *testing.T) {
53 t.Setenv("GODEBUG", os.Getenv("GODEBUG")+",cryptocustomrand=0")
54 testFIPS140Only(t)
55 })
56 t.Run("cryptocustomrand=1", func(t *testing.T) {
57 t.Setenv("GODEBUG", os.Getenv("GODEBUG")+",cryptocustomrand=1")
58 testFIPS140Only(t)
59 })
60 }
61
62 func testFIPS140Only(t *testing.T) {
63 if !fips140only.Enforced() {
64 t.Fatal("FIPS 140-only mode not enforced")
65 }
66 t.Logf("GODEBUG=fips140=only enabled")
67 fips140.ResetServiceIndicator()
68
69 aesBlock, err := aes.NewCipher(make([]byte, 16))
70 if err != nil {
71 t.Fatal(err)
72 }
73 notAESBlock := blockWrap{aesBlock}
74 iv := make([]byte, aes.BlockSize)
75
76 cipher.NewCBCEncrypter(aesBlock, iv)
77 expectPanic(t, func() { cipher.NewCBCEncrypter(notAESBlock, iv) })
78 cipher.NewCBCDecrypter(aesBlock, iv)
79 expectPanic(t, func() { cipher.NewCBCDecrypter(notAESBlock, iv) })
80
81 expectPanic(t, func() { cipher.NewCFBEncrypter(aesBlock, iv) })
82 expectPanic(t, func() { cipher.NewCFBDecrypter(aesBlock, iv) })
83
84 cipher.NewCTR(aesBlock, iv)
85 expectPanic(t, func() { cipher.NewCTR(notAESBlock, iv) })
86
87 expectPanic(t, func() { cipher.NewOFB(aesBlock, iv) })
88
89 expectErr(t, errRet2(cipher.NewGCM(aesBlock)))
90 expectErr(t, errRet2(cipher.NewGCMWithNonceSize(aesBlock, 12)))
91 expectErr(t, errRet2(cipher.NewGCMWithTagSize(aesBlock, 12)))
92 expectNoErr(t, errRet2(cipher.NewGCMWithRandomNonce(aesBlock)))
93
94 expectErr(t, errRet2(des.NewCipher(make([]byte, 8))))
95 expectErr(t, errRet2(des.NewTripleDESCipher(make([]byte, 24))))
96
97 expectErr(t, errRet2(rc4.NewCipher(make([]byte, 16))))
98
99 expectErr(t, errRet2(chacha20poly1305.New(make([]byte, chacha20poly1305.KeySize))))
100 expectErr(t, errRet2(chacha20poly1305.NewX(make([]byte, chacha20poly1305.KeySize))))
101
102 expectPanic(t, func() { md5.New().Sum(nil) })
103 expectErr(t, errRet2(md5.New().Write(make([]byte, 16))))
104 expectPanic(t, func() { md5.Sum([]byte("foo")) })
105
106 expectPanic(t, func() { sha1.New().Sum(nil) })
107 expectErr(t, errRet2(sha1.New().Write(make([]byte, 16))))
108 expectPanic(t, func() { sha1.Sum([]byte("foo")) })
109
110 withApprovedHash(func(h crypto.Hash) { h.New().Sum(nil) })
111 withNonApprovedHash(func(h crypto.Hash) { expectPanic(t, func() { h.New().Sum(nil) }) })
112
113 expectErr(t, errRet2(pbkdf2.Key(sha256.New, "password", make([]byte, 16), 1, 10)))
114 expectErr(t, errRet2(pbkdf2.Key(sha256.New, "password", make([]byte, 10), 1, 14)))
115 withNonApprovedHash(func(h crypto.Hash) {
116 expectErr(t, errRet2(pbkdf2.Key(h.New, "password", make([]byte, 16), 1, 14)))
117 })
118 withApprovedHash(func(h crypto.Hash) {
119 expectNoErr(t, errRet2(pbkdf2.Key(h.New, "password", make([]byte, 16), 1, 14)))
120 })
121
122 expectPanic(t, func() { hmac.New(sha256.New, make([]byte, 10)) })
123 withNonApprovedHash(func(h crypto.Hash) {
124 expectPanic(t, func() { hmac.New(h.New, make([]byte, 16)) })
125 })
126 withApprovedHash(func(h crypto.Hash) { hmac.New(h.New, make([]byte, 16)) })
127
128 expectErr(t, errRet2(hkdf.Key(sha256.New, make([]byte, 10), nil, "", 16)))
129 withNonApprovedHash(func(h crypto.Hash) {
130 expectErr(t, errRet2(hkdf.Key(h.New, make([]byte, 16), nil, "", 16)))
131 })
132 withApprovedHash(func(h crypto.Hash) {
133 expectNoErr(t, errRet2(hkdf.Key(h.New, make([]byte, 16), nil, "", 16)))
134 })
135
136 expectErr(t, errRet2(hkdf.Extract(sha256.New, make([]byte, 10), nil)))
137 withNonApprovedHash(func(h crypto.Hash) {
138 expectErr(t, errRet2(hkdf.Extract(h.New, make([]byte, 16), nil)))
139 })
140 withApprovedHash(func(h crypto.Hash) {
141 expectNoErr(t, errRet2(hkdf.Extract(h.New, make([]byte, 16), nil)))
142 })
143
144 expectErr(t, errRet2(hkdf.Expand(sha256.New, make([]byte, 10), "", 16)))
145 withNonApprovedHash(func(h crypto.Hash) {
146 expectErr(t, errRet2(hkdf.Expand(h.New, make([]byte, 16), "", 16)))
147 })
148 withApprovedHash(func(h crypto.Hash) {
149 expectNoErr(t, errRet2(hkdf.Expand(h.New, make([]byte, 16), "", 16)))
150 })
151
152 expectErr(t, errRet2(rand.Prime(rand.Reader, 10)))
153
154 expectErr(t, dsa.GenerateParameters(&dsa.Parameters{}, rand.Reader, dsa.L1024N160))
155 expectErr(t, dsa.GenerateKey(&dsa.PrivateKey{}, rand.Reader))
156 expectErr(t, errRet3(dsa.Sign(rand.Reader, &dsa.PrivateKey{}, make([]byte, 16))))
157 expectPanic(t, func() {
158 dsa.Verify(&dsa.PublicKey{}, make([]byte, 16), big.NewInt(1), big.NewInt(1))
159 })
160
161 expectErr(t, errRet2(ecdh.X25519().GenerateKey(rand.Reader)))
162 expectErr(t, errRet2(ecdh.X25519().NewPrivateKey(make([]byte, 32))))
163 expectErr(t, errRet2(ecdh.X25519().NewPublicKey(make([]byte, 32))))
164 for _, curve := range []ecdh.Curve{ecdh.P256(), ecdh.P384(), ecdh.P521()} {
165 expectErrIfCustomRand(t, errRet2(curve.GenerateKey(readerWrap{rand.Reader})))
166 k, err := curve.GenerateKey(rand.Reader)
167 if err != nil {
168 t.Fatal(err)
169 }
170 expectNoErr(t, errRet2(curve.NewPrivateKey(k.Bytes())))
171 expectNoErr(t, errRet2(curve.NewPublicKey(k.PublicKey().Bytes())))
172 }
173
174 for _, curve := range []elliptic.Curve{elliptic.P256(), elliptic.P384(), elliptic.P521()} {
175 expectErrIfCustomRand(t, errRet2(ecdsa.GenerateKey(curve, readerWrap{rand.Reader})))
176 k, err := ecdsa.GenerateKey(curve, rand.Reader)
177 if err != nil {
178 t.Fatal(err)
179 }
180
181 expectErrIfCustomRand(t, errRet2(k.Sign(readerWrap{rand.Reader}, make([]byte, 32), nil)))
182 expectErrIfCustomRand(t, errRet2(ecdsa.SignASN1(readerWrap{rand.Reader}, k, make([]byte, 32))))
183 expectErrIfCustomRand(t, errRet3(ecdsa.Sign(readerWrap{rand.Reader}, k, make([]byte, 32))))
184 expectNoErr(t, errRet2(k.Sign(rand.Reader, make([]byte, 32), nil)))
185 expectNoErr(t, errRet2(ecdsa.SignASN1(rand.Reader, k, make([]byte, 32))))
186 expectNoErr(t, errRet3(ecdsa.Sign(rand.Reader, k, make([]byte, 32))))
187
188 withNonApprovedHash(func(h crypto.Hash) {
189 expectErr(t, errRet2(k.Sign(nil, make([]byte, h.Size()), h)))
190 })
191 withApprovedHash(func(h crypto.Hash) {
192 expectNoErr(t, errRet2(k.Sign(nil, make([]byte, h.Size()), h)))
193 })
194 }
195 customCurve := &elliptic.CurveParams{Name: "custom", P: big.NewInt(1)}
196 expectErr(t, errRet2(ecdsa.GenerateKey(customCurve, rand.Reader)))
197
198 _, ed25519Key, err := ed25519.GenerateKey(rand.Reader)
199 if err != nil {
200 t.Fatal(err)
201 }
202 expectNoErr(t, errRet2(ed25519Key.Sign(nil, make([]byte, 32), crypto.Hash(0))))
203 expectNoErr(t, errRet2(ed25519Key.Sign(nil, make([]byte, 64), crypto.SHA512)))
204
205 expectErr(t, errRet2(ed25519Key.Sign(nil, make([]byte, 32), &ed25519.Options{
206 Context: "test",
207 })))
208 expectNoErr(t, errRet2(ed25519Key.Sign(nil, make([]byte, 64), &ed25519.Options{
209 Hash: crypto.SHA512, Context: "test",
210 })))
211 expectNoErr(t, errRet2(ed25519Key.Sign(nil, make([]byte, 64), &ed25519.Options{
212 Hash: crypto.SHA512,
213 })))
214
215 expectErr(t, errRet2(rsa.GenerateMultiPrimeKey(rand.Reader, 3, 2048)))
216 expectErr(t, errRet2(rsa.GenerateKey(rand.Reader, 1024)))
217 expectErr(t, errRet2(rsa.GenerateKey(rand.Reader, 2049)))
218 expectErrIfCustomRand(t, errRet2(rsa.GenerateKey(readerWrap{rand.Reader}, 2048)))
219 rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
220 expectNoErr(t, err)
221
222 smallKey := parseKey(testingKey(`-----BEGIN RSA TESTING KEY-----
223 MIICXQIBAAKBgQDMrln6XoAa3Rjts+kRi5obbP86qSf/562RcuDO+yMXeTLHfi4M
224 8ubyhoFY+UKBCGBLmmTO7ikbvQgdipkT3xVkU8nM3XTW4sxrnw0X5QXsl4PGlMo0
225 5UufxYyQxe7bbjuwFz2XnN6Jz4orpOfO0s36/KVHj9lZRl+REpr/Jy+nJQIDAQAB
226 AoGAJ9WEwGO01cWSzOwXH2mGX/EKCQ4TsUuS7XwogU/B6BcXyVhmuPFq/ecsdDbq
227 ePc62mvdU6JpELNsyWcIXKQtYsRgJHxNS+KJkCQIq6YeiAWRG0XL6q+qVj+HtT8a
228 1Qrmul9ZBd23Y9wLF8pg/xWDQYvb8DPAb/xJ0e/KEBZcWU8CQQDXFCFCGpCfwyxY
229 Cq8G/3B94D9UYwk5mK6jRIH5m8LbaX9bKKetf8+If8TWVgeuiRjjN4WEQ78lPoSg
230 3Fsz2qs3AkEA85/JCudNUf2FnY+T6h1c/2SWekZiZ1NS4lCh/C7iYuAN3oa8zGkf
231 gjjR5e0+Z8rUAcZkTukxyLLaNqy6rs9GgwJAVR6pXvEGhcQHe7yWso1LpvWl+q7L
232 StkrXIBTdEb54j4pYhl/6wFnUB1I+I7JsYCeseYaWFM7hfDtKoCrM6V6FwJBANxh
233 KmfmnJcSkw/YlaEuNrYAs+6gRNvbEBsRfba2Yqu2qlUl5Ruz7IDMDXPEjLMvU2DX
234 ql2HrTU0NRlIXwdLESkCQQDGJ54H6WK1eE1YvtxCaLm28zmogcFlvc21pym+PpM1
235 bXVL8iKLrG91IYQByUHZIn3WVAd2bfi4MfKagRt0ggd4
236 -----END RSA TESTING KEY-----`))
237
238 expectNoErr(t, errRet2(rsaKey.Sign(rand.Reader, make([]byte, 32), crypto.SHA256)))
239 expectErr(t, errRet2(smallKey.Sign(rand.Reader, make([]byte, 32), crypto.SHA256)))
240 expectErr(t, errRet2(rsaKey.Sign(rand.Reader, make([]byte, 20), crypto.SHA1)))
241
242 expectNoErr(t, errRet2(rsaKey.Sign(readerWrap{rand.Reader}, make([]byte, 32), crypto.SHA256)))
243
244 sigPKCS1v15, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, make([]byte, 32))
245 expectNoErr(t, err)
246 expectErr(t, errRet2(rsa.SignPKCS1v15(rand.Reader, smallKey, crypto.SHA256, make([]byte, 32))))
247 expectErr(t, errRet2(rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA1, make([]byte, 20))))
248
249 expectNoErr(t, errRet2(rsa.SignPKCS1v15(readerWrap{rand.Reader}, rsaKey, crypto.SHA256, make([]byte, 32))))
250 expectErr(t, errRet2(rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.Hash(0), make([]byte, 32))))
251
252 expectNoErr(t, rsa.VerifyPKCS1v15(&rsaKey.PublicKey, crypto.SHA256, make([]byte, 32), sigPKCS1v15))
253 expectErr(t, rsa.VerifyPKCS1v15(&smallKey.PublicKey, crypto.SHA256, make([]byte, 32), sigPKCS1v15))
254 expectErr(t, rsa.VerifyPKCS1v15(&rsaKey.PublicKey, crypto.SHA1, make([]byte, 20), sigPKCS1v15))
255 expectErr(t, rsa.VerifyPKCS1v15(&rsaKey.PublicKey, crypto.Hash(0), make([]byte, 32), sigPKCS1v15))
256
257 sigPSS, err := rsa.SignPSS(rand.Reader, rsaKey, crypto.SHA256, make([]byte, 32), nil)
258 expectNoErr(t, err)
259 expectErr(t, errRet2(rsa.SignPSS(rand.Reader, smallKey, crypto.SHA256, make([]byte, 32), nil)))
260 expectErr(t, errRet2(rsa.SignPSS(rand.Reader, rsaKey, crypto.SHA1, make([]byte, 20), nil)))
261 expectErr(t, errRet2(rsa.SignPSS(readerWrap{rand.Reader}, rsaKey, crypto.SHA256, make([]byte, 32), nil)))
262
263 expectNoErr(t, rsa.VerifyPSS(&rsaKey.PublicKey, crypto.SHA256, make([]byte, 32), sigPSS, nil))
264 expectErr(t, rsa.VerifyPSS(&smallKey.PublicKey, crypto.SHA256, make([]byte, 32), sigPSS, nil))
265 expectErr(t, rsa.VerifyPSS(&rsaKey.PublicKey, crypto.SHA1, make([]byte, 20), sigPSS, nil))
266
267 oaepMsg := make([]byte, 32)
268 oaepCiphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &rsaKey.PublicKey, oaepMsg, nil)
269 expectNoErr(t, err)
270 expectErr(t, errRet2(rsa.EncryptOAEP(sha256.New(), rand.Reader, &smallKey.PublicKey, oaepMsg, nil)))
271 expectErr(t, errRet2(rsa.EncryptOAEP(sha1.New(), rand.Reader, &rsaKey.PublicKey, oaepMsg, nil)))
272 expectErr(t, errRet2(rsa.EncryptOAEP(sha256.New(), readerWrap{rand.Reader}, &rsaKey.PublicKey, oaepMsg, nil)))
273 expectNoErr(t, errRet2(rsa.EncryptOAEPWithOptions(rand.Reader, &rsaKey.PublicKey, oaepMsg, &rsa.OAEPOptions{Hash: crypto.SHA256})))
274 withApprovedHash(func(h crypto.Hash) {
275 expectNoErr(t, errRet2(rsa.EncryptOAEPWithOptions(rand.Reader, &rsaKey.PublicKey, oaepMsg, &rsa.OAEPOptions{Hash: crypto.SHA256, MGFHash: h})))
276 })
277 withNonApprovedHash(func(h crypto.Hash) {
278 expectErr(t, errRet2(rsa.EncryptOAEPWithOptions(rand.Reader, &rsaKey.PublicKey, oaepMsg, &rsa.OAEPOptions{Hash: h})))
279 expectErr(t, errRet2(rsa.EncryptOAEPWithOptions(rand.Reader, &rsaKey.PublicKey, oaepMsg, &rsa.OAEPOptions{Hash: crypto.SHA256, MGFHash: h})))
280 })
281
282 expectNoErr(t, errRet2(rsa.DecryptOAEP(sha256.New(), nil, rsaKey, oaepCiphertext, nil)))
283 expectErr(t, errRet2(rsa.DecryptOAEP(sha256.New(), nil, smallKey, oaepCiphertext, nil)))
284 expectErr(t, errRet2(rsa.DecryptOAEP(sha1.New(), nil, rsaKey, oaepCiphertext, nil)))
285 expectNoErr(t, errRet2(rsaKey.Decrypt(nil, oaepCiphertext, &rsa.OAEPOptions{Hash: crypto.SHA256})))
286 withNonApprovedHash(func(h crypto.Hash) {
287 expectErr(t, errRet2(rsaKey.Decrypt(nil, oaepCiphertext, &rsa.OAEPOptions{Hash: h})))
288 expectErr(t, errRet2(rsaKey.Decrypt(nil, oaepCiphertext, &rsa.OAEPOptions{Hash: crypto.SHA256, MGFHash: h})))
289 })
290
291 k, err := mlkem.GenerateKey768()
292 expectNoErr(t, err)
293 expectErr(t, errRet3(mlkemtest.Encapsulate768(k.EncapsulationKey(), make([]byte, 32))))
294 k1024, err := mlkem.GenerateKey1024()
295 expectNoErr(t, err)
296 expectErr(t, errRet3(mlkemtest.Encapsulate1024(k1024.EncapsulationKey(), make([]byte, 32))))
297
298 for _, kem := range []hpke.KEM{
299 hpke.DHKEM(ecdh.P256()),
300 hpke.DHKEM(ecdh.P384()),
301 hpke.DHKEM(ecdh.P521()),
302 hpke.MLKEM768(),
303 hpke.MLKEM1024(),
304 hpke.MLKEM768P256(),
305 hpke.MLKEM1024P384(),
306 hpke.MLKEM768X25519(),
307 } {
308 t.Run(fmt.Sprintf("HKPE KEM %04x", kem.ID()), func(t *testing.T) {
309 k, err := kem.GenerateKey()
310 expectNoErr(t, err)
311 expectNoErr(t, errRet2(kem.DeriveKeyPair(make([]byte, 64))))
312 kb, err := k.Bytes()
313 expectNoErr(t, err)
314 expectNoErr(t, errRet2(kem.NewPrivateKey(kb)))
315 expectNoErr(t, errRet2(kem.NewPublicKey(k.PublicKey().Bytes())))
316
317 cryptotest.MustMinimumFIPS140ModuleVersion(t, "v1.26.0")
318 c, err := hpke.Seal(k.PublicKey(), hpke.HKDFSHA256(), hpke.AES128GCM(), nil, nil)
319 expectNoErr(t, err)
320 _, err = hpke.Open(k, hpke.HKDFSHA256(), hpke.AES128GCM(), nil, c)
321 expectNoErr(t, err)
322 })
323 }
324 expectErr(t, errRet2(hpke.DHKEM(ecdh.X25519()).GenerateKey()))
325 expectErr(t, errRet2(hpke.DHKEM(ecdh.X25519()).DeriveKeyPair(make([]byte, 64))))
326 expectErr(t, errRet2(hpke.DHKEM(ecdh.X25519()).NewPrivateKey(make([]byte, 32))))
327 expectErr(t, errRet2(hpke.DHKEM(ecdh.X25519()).NewPublicKey(make([]byte, 32))))
328 hpkeK, err := hpke.MLKEM768().GenerateKey()
329 expectNoErr(t, err)
330 expectErr(t, errRet2(hpke.Seal(hpkeK.PublicKey(), hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), nil, nil)))
331 expectErr(t, errRet2(hpke.Open(hpkeK, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), nil, make([]byte, 2000))))
332
333
334
335 if !fips140.ServiceIndicator() {
336 t.Errorf("service indicator not set")
337 }
338 }
339
340 type blockWrap struct {
341 cipher.Block
342 }
343
344 type readerWrap struct {
345 io.Reader
346 }
347
348 func withApprovedHash(f func(crypto.Hash)) {
349 f(crypto.SHA224)
350 f(crypto.SHA256)
351 f(crypto.SHA384)
352 f(crypto.SHA512)
353 f(crypto.SHA3_224)
354 f(crypto.SHA3_256)
355 f(crypto.SHA3_384)
356 f(crypto.SHA3_512)
357 f(crypto.SHA512_224)
358 f(crypto.SHA512_256)
359 }
360
361 func withNonApprovedHash(f func(crypto.Hash)) {
362 f(crypto.MD5)
363 f(crypto.SHA1)
364 }
365
366 func expectPanic(t *testing.T, f func()) {
367 t.Helper()
368 defer func() {
369 t.Helper()
370 if err := recover(); err == nil {
371 t.Errorf("expected panic")
372 } else {
373 if s, ok := err.(string); !ok || !strings.Contains(s, "FIPS 140-only") {
374 t.Errorf("unexpected panic: %v", err)
375 }
376 }
377 }()
378 f()
379 }
380
381 var cryptocustomrand = godebug.New("cryptocustomrand")
382
383 func expectErr(t *testing.T, err error) {
384 t.Helper()
385 if err == nil {
386 t.Errorf("expected error")
387 } else if !strings.Contains(err.Error(), "FIPS 140-only") {
388 t.Errorf("unexpected error: %v", err)
389 }
390 }
391
392 func expectNoErr(t *testing.T, err error) {
393 t.Helper()
394 if err != nil {
395 t.Errorf("unexpected error: %v", err)
396 }
397 }
398
399 func expectErrIfCustomRand(t *testing.T, err error) {
400 t.Helper()
401 if cryptocustomrand.Value() == "1" {
402 expectErr(t, err)
403 } else {
404 expectNoErr(t, err)
405 }
406 }
407
408 func errRet2[T any](_ T, err error) error {
409 return err
410 }
411
412 func errRet3[T any](_, _ T, err error) error {
413 return err
414 }
415
416 func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
417
418 func parseKey(s string) *rsa.PrivateKey {
419 p, _ := pem.Decode([]byte(s))
420 k, err := x509.ParsePKCS1PrivateKey(p.Bytes)
421 if err != nil {
422 panic(err)
423 }
424 return k
425 }
426
View as plain text