Skip to content

Commit 00ee186

Browse files
FiloSottilegopherbot
authored andcommitted
crypto/internal/constanttime: expose intrinsics to the FIPS 140-3 packages
Intrinsifying things inside the module (crypto/internal/fips140/subtle) is asking for trouble, as the import paths are rewritten by the GOFIPS140 mechanism, and we might have to support multiple modules in the future. Importing crypto/subtle from inside a FIPS 140-3 module is not allowed, and is basically asking for circular dependencies. Instead, break off the intrinsics into their own package (crypto/internal/constanttime), and keep the byte slice operations in crypto/internal/fips140/subtle. crypto/subtle then becomes a thin dispatch layer. Change-Id: I6a6a6964cd5cb5ad06e9d1679201447f5a811da4 Reviewed-on: https://go-review.googlesource.com/c/go/+/716120 Reviewed-by: Keith Randall <[email protected]> Reviewed-by: Michael Knyszek <[email protected]> Reviewed-by: Keith Randall <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Filippo Valsorda <[email protected]> Reviewed-by: Jorropo <[email protected]>
1 parent 388c41c commit 00ee186

File tree

14 files changed

+110
-100
lines changed

14 files changed

+110
-100
lines changed

src/cmd/compile/internal/ssagen/intrinsics.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,10 +1603,10 @@ func initIntrinsics(cfg *intrinsicBuildConfig) {
16031603
},
16041604
sys.AMD64)
16051605

1606-
/******** crypto/subtle ********/
1607-
// We implement a superset of the ConstantTimeSelect promise:
1608-
// ConstantTimeSelect returns x if v != 0 and y if v == 0.
1609-
add("crypto/subtle", "ConstantTimeSelect",
1606+
/******** crypto/internal/constanttime ********/
1607+
// We implement a superset of the Select promise:
1608+
// Select returns x if v != 0 and y if v == 0.
1609+
add("crypto/internal/constanttime", "Select",
16101610
func(s *state, n *ir.CallExpr, args []*ssa.Value) *ssa.Value {
16111611
v, x, y := args[0], args[1], args[2]
16121612

@@ -1627,7 +1627,7 @@ func initIntrinsics(cfg *intrinsicBuildConfig) {
16271627
return s.newValue3(ssa.OpCondSelect, types.Types[types.TINT], x, y, check)
16281628
},
16291629
sys.ArchAMD64, sys.ArchARM64, sys.ArchLoong64, sys.ArchPPC64, sys.ArchPPC64LE, sys.ArchWasm) // all with CMOV support.
1630-
add("crypto/subtle", "constantTimeBoolToUint8",
1630+
add("crypto/internal/constanttime", "boolToUint8",
16311631
func(s *state, n *ir.CallExpr, args []*ssa.Value) *ssa.Value {
16321632
return s.newValue1(ssa.OpCvtBoolToUint8, types.Types[types.TUINT8], args[0])
16331633
},

src/cmd/compile/internal/ssagen/intrinsics_test.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
4242
{"386", "math/bits", "TrailingZeros8"}: struct{}{},
4343
{"386", "runtime", "KeepAlive"}: struct{}{},
4444
{"386", "runtime", "slicebytetostringtmp"}: struct{}{},
45-
{"386", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
45+
{"386", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
4646
{"amd64", "internal/runtime/atomic", "And"}: struct{}{},
4747
{"amd64", "internal/runtime/atomic", "And32"}: struct{}{},
4848
{"amd64", "internal/runtime/atomic", "And64"}: struct{}{},
@@ -189,8 +189,8 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
189189
{"amd64", "sync/atomic", "SwapUint32"}: struct{}{},
190190
{"amd64", "sync/atomic", "SwapUint64"}: struct{}{},
191191
{"amd64", "sync/atomic", "SwapUintptr"}: struct{}{},
192-
{"amd64", "crypto/subtle", "ConstantTimeSelect"}: struct{}{},
193-
{"amd64", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
192+
{"amd64", "crypto/internal/constanttime", "Select"}: struct{}{},
193+
{"amd64", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
194194
{"arm", "internal/runtime/sys", "Bswap32"}: struct{}{},
195195
{"arm", "internal/runtime/sys", "Bswap64"}: struct{}{},
196196
{"arm", "internal/runtime/sys", "GetCallerPC"}: struct{}{},
@@ -219,7 +219,7 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
219219
{"arm", "math/bits", "TrailingZeros8"}: struct{}{},
220220
{"arm", "runtime", "KeepAlive"}: struct{}{},
221221
{"arm", "runtime", "slicebytetostringtmp"}: struct{}{},
222-
{"arm", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
222+
{"arm", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
223223
{"arm64", "internal/runtime/atomic", "And"}: struct{}{},
224224
{"arm64", "internal/runtime/atomic", "And32"}: struct{}{},
225225
{"arm64", "internal/runtime/atomic", "And64"}: struct{}{},
@@ -364,8 +364,8 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
364364
{"arm64", "sync/atomic", "SwapUint32"}: struct{}{},
365365
{"arm64", "sync/atomic", "SwapUint64"}: struct{}{},
366366
{"arm64", "sync/atomic", "SwapUintptr"}: struct{}{},
367-
{"arm64", "crypto/subtle", "ConstantTimeSelect"}: struct{}{},
368-
{"arm64", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
367+
{"arm64", "crypto/internal/constanttime", "Select"}: struct{}{},
368+
{"arm64", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
369369
{"loong64", "internal/runtime/atomic", "And"}: struct{}{},
370370
{"loong64", "internal/runtime/atomic", "And32"}: struct{}{},
371371
{"loong64", "internal/runtime/atomic", "And64"}: struct{}{},
@@ -512,8 +512,8 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
512512
{"loong64", "sync/atomic", "SwapUint32"}: struct{}{},
513513
{"loong64", "sync/atomic", "SwapUint64"}: struct{}{},
514514
{"loong64", "sync/atomic", "SwapUintptr"}: struct{}{},
515-
{"loong64", "crypto/subtle", "ConstantTimeSelect"}: struct{}{},
516-
{"loong64", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
515+
{"loong64", "crypto/internal/constanttime", "Select"}: struct{}{},
516+
{"loong64", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
517517
{"mips", "internal/runtime/atomic", "And"}: struct{}{},
518518
{"mips", "internal/runtime/atomic", "And8"}: struct{}{},
519519
{"mips", "internal/runtime/atomic", "Cas"}: struct{}{},
@@ -585,7 +585,7 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
585585
{"mips", "sync/atomic", "SwapInt32"}: struct{}{},
586586
{"mips", "sync/atomic", "SwapUint32"}: struct{}{},
587587
{"mips", "sync/atomic", "SwapUintptr"}: struct{}{},
588-
{"mips", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
588+
{"mips", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
589589
{"mips64", "internal/runtime/atomic", "And"}: struct{}{},
590590
{"mips64", "internal/runtime/atomic", "And8"}: struct{}{},
591591
{"mips64", "internal/runtime/atomic", "Cas"}: struct{}{},
@@ -674,7 +674,7 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
674674
{"mips64", "sync/atomic", "SwapUint32"}: struct{}{},
675675
{"mips64", "sync/atomic", "SwapUint64"}: struct{}{},
676676
{"mips64", "sync/atomic", "SwapUintptr"}: struct{}{},
677-
{"mips64", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
677+
{"mips64", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
678678
{"mips64le", "internal/runtime/atomic", "And"}: struct{}{},
679679
{"mips64le", "internal/runtime/atomic", "And8"}: struct{}{},
680680
{"mips64le", "internal/runtime/atomic", "Cas"}: struct{}{},
@@ -763,7 +763,7 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
763763
{"mips64le", "sync/atomic", "SwapUint32"}: struct{}{},
764764
{"mips64le", "sync/atomic", "SwapUint64"}: struct{}{},
765765
{"mips64le", "sync/atomic", "SwapUintptr"}: struct{}{},
766-
{"mips64le", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
766+
{"mips64le", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
767767
{"mipsle", "internal/runtime/atomic", "And"}: struct{}{},
768768
{"mipsle", "internal/runtime/atomic", "And8"}: struct{}{},
769769
{"mipsle", "internal/runtime/atomic", "Cas"}: struct{}{},
@@ -835,7 +835,7 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
835835
{"mipsle", "sync/atomic", "SwapInt32"}: struct{}{},
836836
{"mipsle", "sync/atomic", "SwapUint32"}: struct{}{},
837837
{"mipsle", "sync/atomic", "SwapUintptr"}: struct{}{},
838-
{"mipsle", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
838+
{"mipsle", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
839839
{"ppc64", "internal/runtime/atomic", "And"}: struct{}{},
840840
{"ppc64", "internal/runtime/atomic", "And8"}: struct{}{},
841841
{"ppc64", "internal/runtime/atomic", "Cas"}: struct{}{},
@@ -960,8 +960,8 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
960960
{"ppc64", "sync/atomic", "SwapUint32"}: struct{}{},
961961
{"ppc64", "sync/atomic", "SwapUint64"}: struct{}{},
962962
{"ppc64", "sync/atomic", "SwapUintptr"}: struct{}{},
963-
{"ppc64", "crypto/subtle", "ConstantTimeSelect"}: struct{}{},
964-
{"ppc64", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
963+
{"ppc64", "crypto/internal/constanttime", "Select"}: struct{}{},
964+
{"ppc64", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
965965
{"ppc64le", "internal/runtime/atomic", "And"}: struct{}{},
966966
{"ppc64le", "internal/runtime/atomic", "And8"}: struct{}{},
967967
{"ppc64le", "internal/runtime/atomic", "Cas"}: struct{}{},
@@ -1086,8 +1086,8 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
10861086
{"ppc64le", "sync/atomic", "SwapUint32"}: struct{}{},
10871087
{"ppc64le", "sync/atomic", "SwapUint64"}: struct{}{},
10881088
{"ppc64le", "sync/atomic", "SwapUintptr"}: struct{}{},
1089-
{"ppc64le", "crypto/subtle", "ConstantTimeSelect"}: struct{}{},
1090-
{"ppc64le", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
1089+
{"ppc64le", "crypto/internal/constanttime", "Select"}: struct{}{},
1090+
{"ppc64le", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
10911091
{"riscv64", "internal/runtime/atomic", "And"}: struct{}{},
10921092
{"riscv64", "internal/runtime/atomic", "And8"}: struct{}{},
10931093
{"riscv64", "internal/runtime/atomic", "Cas"}: struct{}{},
@@ -1208,7 +1208,7 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
12081208
{"riscv64", "sync/atomic", "SwapUint32"}: struct{}{},
12091209
{"riscv64", "sync/atomic", "SwapUint64"}: struct{}{},
12101210
{"riscv64", "sync/atomic", "SwapUintptr"}: struct{}{},
1211-
{"riscv64", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
1211+
{"riscv64", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
12121212
{"s390x", "internal/runtime/atomic", "And"}: struct{}{},
12131213
{"s390x", "internal/runtime/atomic", "And8"}: struct{}{},
12141214
{"s390x", "internal/runtime/atomic", "Cas"}: struct{}{},
@@ -1327,7 +1327,7 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
13271327
{"s390x", "sync/atomic", "SwapUint32"}: struct{}{},
13281328
{"s390x", "sync/atomic", "SwapUint64"}: struct{}{},
13291329
{"s390x", "sync/atomic", "SwapUintptr"}: struct{}{},
1330-
{"s390x", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
1330+
{"s390x", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
13311331
{"wasm", "internal/runtime/sys", "GetCallerPC"}: struct{}{},
13321332
{"wasm", "internal/runtime/sys", "GetCallerSP"}: struct{}{},
13331333
{"wasm", "internal/runtime/sys", "GetClosurePtr"}: struct{}{},
@@ -1363,8 +1363,8 @@ var wantIntrinsics = map[testIntrinsicKey]struct{}{
13631363
{"wasm", "math/bits", "TrailingZeros8"}: struct{}{},
13641364
{"wasm", "runtime", "KeepAlive"}: struct{}{},
13651365
{"wasm", "runtime", "slicebytetostringtmp"}: struct{}{},
1366-
{"wasm", "crypto/subtle", "ConstantTimeSelect"}: struct{}{},
1367-
{"wasm", "crypto/subtle", "constantTimeBoolToUint8"}: struct{}{},
1366+
{"wasm", "crypto/internal/constanttime", "Select"}: struct{}{},
1367+
{"wasm", "crypto/internal/constanttime", "boolToUint8"}: struct{}{},
13681368
}
13691369

13701370
func TestIntrinsics(t *testing.T) {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package constanttime
6+
7+
// The functions in this package are compiler intrinsics for constant-time
8+
// operations. They are exposed by crypto/subtle and used directly by the
9+
// FIPS 140-3 module.
10+
11+
// Select returns x if v == 1 and y if v == 0.
12+
// Its behavior is undefined if v takes any other value.
13+
func Select(v, x, y int) int {
14+
// This is intrinsicified on arches with CMOV.
15+
// It implements the following superset behavior:
16+
// ConstantTimeSelect returns x if v != 0 and y if v == 0.
17+
// Do the same here to avoid non portable UB.
18+
v = int(boolToUint8(v != 0))
19+
return ^(v-1)&x | (v-1)&y
20+
}
21+
22+
// ByteEq returns 1 if x == y and 0 otherwise.
23+
func ByteEq(x, y uint8) int {
24+
return int(boolToUint8(x == y))
25+
}
26+
27+
// Eq returns 1 if x == y and 0 otherwise.
28+
func Eq(x, y int32) int {
29+
return int(boolToUint8(x == y))
30+
}
31+
32+
// LessOrEq returns 1 if x <= y and 0 otherwise.
33+
// Its behavior is undefined if x or y are negative or > 2**31 - 1.
34+
func LessOrEq(x, y int) int {
35+
return int(boolToUint8(x <= y))
36+
}
37+
38+
// boolToUint8 is a compiler intrinsic.
39+
// It returns 1 for true and 0 for false.
40+
func boolToUint8(b bool) uint8 {
41+
panic("unreachable; must be intrinsicified")
42+
}

src/crypto/internal/fips140/edwards25519/tables.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44

55
package edwards25519
66

7-
import (
8-
"crypto/internal/fips140/subtle"
9-
)
7+
import "crypto/internal/constanttime"
108

119
// A dynamic lookup table for variable-base, constant-time scalar muls.
1210
type projLookupTable struct {
@@ -95,7 +93,7 @@ func (v *projLookupTable) SelectInto(dest *projCached, x int8) {
9593
dest.Zero()
9694
for j := 1; j <= 8; j++ {
9795
// Set dest = j*Q if |x| = j
98-
cond := subtle.ConstantTimeByteEq(xabs, uint8(j))
96+
cond := constanttime.ByteEq(xabs, uint8(j))
9997
dest.Select(&v.points[j-1], dest, cond)
10098
}
10199
// Now dest = |x|*Q, conditionally negate to get x*Q
@@ -111,7 +109,7 @@ func (v *affineLookupTable) SelectInto(dest *affineCached, x int8) {
111109
dest.Zero()
112110
for j := 1; j <= 8; j++ {
113111
// Set dest = j*Q if |x| = j
114-
cond := subtle.ConstantTimeByteEq(xabs, uint8(j))
112+
cond := constanttime.ByteEq(xabs, uint8(j))
115113
dest.Select(&v.points[j-1], dest, cond)
116114
}
117115
// Now dest = |x|*Q, conditionally negate to get x*Q

src/crypto/internal/fips140/nistec/generate.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ const tmplNISTEC = `// Copyright 2022 The Go Authors. All rights reserved.
140140
package nistec
141141
142142
import (
143+
"crypto/internal/constanttime"
143144
"crypto/internal/fips140/nistec/fiat"
144-
"crypto/internal/fips140/subtle"
145145
"errors"
146146
"sync"
147147
)
@@ -467,7 +467,7 @@ func (table *{{.p}}Table) Select(p *{{.P}}Point, n uint8) {
467467
}
468468
p.Set(New{{.P}}Point())
469469
for i := uint8(1); i < 16; i++ {
470-
cond := subtle.ConstantTimeByteEq(i, n)
470+
cond := constanttime.ByteEq(i, n)
471471
p.Select(table[i-1], p, cond)
472472
}
473473
}

src/crypto/internal/fips140/nistec/p224.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/crypto/internal/fips140/nistec/p256.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
package nistec
88

99
import (
10+
"crypto/internal/constanttime"
1011
"crypto/internal/fips140/nistec/fiat"
11-
"crypto/internal/fips140/subtle"
1212
"crypto/internal/fips140deps/byteorder"
1313
"crypto/internal/fips140deps/cpu"
1414
"errors"
@@ -458,7 +458,7 @@ func (table *p256Table) Select(p *P256Point, n uint8) {
458458
}
459459
p.Set(NewP256Point())
460460
for i := uint8(1); i <= 16; i++ {
461-
cond := subtle.ConstantTimeByteEq(i, n)
461+
cond := constanttime.ByteEq(i, n)
462462
p.Select(&table[i-1], p, cond)
463463
}
464464
}
@@ -553,7 +553,7 @@ func (table *p256AffineTable) Select(p *p256AffinePoint, n uint8) {
553553
panic("nistec: internal error: p256AffineTable.Select called with out-of-bounds value")
554554
}
555555
for i := uint8(1); i <= 32; i++ {
556-
cond := subtle.ConstantTimeByteEq(i, n)
556+
cond := constanttime.ByteEq(i, n)
557557
p.x.Select(&table[i-1].x, &p.x, cond)
558558
p.y.Select(&table[i-1].y, &p.y, cond)
559559
}
@@ -618,7 +618,7 @@ func (p *P256Point) ScalarBaseMult(scalar []byte) (*P256Point, error) {
618618
// the point at infinity (because infinity can't be represented in affine
619619
// coordinates). Here we conditionally set p to the infinity if sel is zero.
620620
// In the loop, that's handled by AddAffine.
621-
selIsZero := subtle.ConstantTimeByteEq(sel, 0)
621+
selIsZero := constanttime.ByteEq(sel, 0)
622622
p.Select(NewP256Point(), t.Projective(), selIsZero)
623623

624624
for index >= 5 {
@@ -636,7 +636,7 @@ func (p *P256Point) ScalarBaseMult(scalar []byte) (*P256Point, error) {
636636
table := &p256GeneratorTables[(index+1)/6]
637637
table.Select(t, sel)
638638
t.Negate(sign)
639-
selIsZero := subtle.ConstantTimeByteEq(sel, 0)
639+
selIsZero := constanttime.ByteEq(sel, 0)
640640
p.AddAffine(p, t, selIsZero)
641641
}
642642

src/crypto/internal/fips140/nistec/p384.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/crypto/internal/fips140/nistec/p521.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/crypto/internal/fips140/rsa/pkcs1v22.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package rsa
99

1010
import (
1111
"bytes"
12+
"crypto/internal/constanttime"
1213
"crypto/internal/fips140"
1314
"crypto/internal/fips140/drbg"
1415
"crypto/internal/fips140/sha256"
@@ -432,7 +433,7 @@ func DecryptOAEP(hash, mgfHash hash.Hash, priv *PrivateKey, ciphertext []byte, l
432433
hash.Write(label)
433434
lHash := hash.Sum(nil)
434435

435-
firstByteIsZero := subtle.ConstantTimeByteEq(em[0], 0)
436+
firstByteIsZero := constanttime.ByteEq(em[0], 0)
436437

437438
seed := em[1 : hash.Size()+1]
438439
db := em[hash.Size()+1:]
@@ -458,11 +459,11 @@ func DecryptOAEP(hash, mgfHash hash.Hash, priv *PrivateKey, ciphertext []byte, l
458459
rest := db[hash.Size():]
459460

460461
for i := 0; i < len(rest); i++ {
461-
equals0 := subtle.ConstantTimeByteEq(rest[i], 0)
462-
equals1 := subtle.ConstantTimeByteEq(rest[i], 1)
463-
index = subtle.ConstantTimeSelect(lookingForIndex&equals1, i, index)
464-
lookingForIndex = subtle.ConstantTimeSelect(equals1, 0, lookingForIndex)
465-
invalid = subtle.ConstantTimeSelect(lookingForIndex&^equals0, 1, invalid)
462+
equals0 := constanttime.ByteEq(rest[i], 0)
463+
equals1 := constanttime.ByteEq(rest[i], 1)
464+
index = constanttime.Select(lookingForIndex&equals1, i, index)
465+
lookingForIndex = constanttime.Select(equals1, 0, lookingForIndex)
466+
invalid = constanttime.Select(lookingForIndex&^equals0, 1, invalid)
466467
}
467468

468469
if firstByteIsZero&lHash2Good&^invalid&^lookingForIndex != 1 {

0 commit comments

Comments
 (0)