Improve test coverage of package hsm

This commit is contained in:
Jan Dittberner 2022-05-01 12:36:17 +02:00 committed by Jan Dittberner
parent 5776723fa2
commit c532ec436a
6 changed files with 481 additions and 75 deletions

View file

@ -20,6 +20,7 @@ package hsm
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"github.com/ThalesIgnite/crypto11" "github.com/ThalesIgnite/crypto11"
@ -29,7 +30,8 @@ import (
type ctxKey int type ctxKey int
const ( const (
ctxP11Contexts ctxKey = iota ctxCADirectory ctxKey = iota
ctxP11Contexts
ctxSetupMode ctxSetupMode
ctxSignerConfig ctxSignerConfig
ctxVerboseLogging ctxVerboseLogging
@ -37,6 +39,12 @@ const (
type ConfigOption func(ctx context.Context) context.Context type ConfigOption func(ctx context.Context) context.Context
func CADirectoryOption(path string) func(ctx context.Context) context.Context {
return func(ctx context.Context) context.Context {
return context.WithValue(ctx, ctxCADirectory, path)
}
}
func CaConfigOption(signerConfig *config.SignerConfig) func(context.Context) context.Context { func CaConfigOption(signerConfig *config.SignerConfig) func(context.Context) context.Context {
return func(ctx context.Context) context.Context { return func(ctx context.Context) context.Context {
return context.WithValue(ctx, ctxSignerConfig, signerConfig) return context.WithValue(ctx, ctxSignerConfig, signerConfig)
@ -114,3 +122,27 @@ func GetP11Context(ctx context.Context, entry *config.CaCertificateEntry) (*cryp
return p11Context, nil return p11Context, nil
} }
func CloseP11Contexts(ctx context.Context) error {
contexts, ok := ctx.Value(ctxP11Contexts).(map[string]*crypto11.Context)
if !ok {
return errors.New("type assertion failed, use hsm.SetupContext first")
}
seen := make(map[*crypto11.Context]struct{}, 0)
for _, p11Context := range contexts {
if _, ok := seen[p11Context]; ok {
continue
}
seen[p11Context] = struct{}{}
err := p11Context.Close()
if err != nil {
return fmt.Errorf("could not close context: %w", err)
}
}
return nil
}

View file

@ -36,49 +36,49 @@ import (
func TestCaConfigOption(t *testing.T) { func TestCaConfigOption(t *testing.T) {
testSignerConfig := config.SignerConfig{} testSignerConfig := config.SignerConfig{}
theContext := hsm.SetupContext(hsm.CaConfigOption(&testSignerConfig)) ctx := hsm.SetupContext(hsm.CaConfigOption(&testSignerConfig))
assert.Equal(t, &testSignerConfig, hsm.GetSignerConfig(theContext)) assert.Equal(t, &testSignerConfig, hsm.GetSignerConfig(ctx))
} }
func TestGetSignerConfig_empty(t *testing.T) { func TestGetSignerConfig_empty(t *testing.T) {
theContext := hsm.SetupContext() ctx := hsm.SetupContext()
assert.Nil(t, hsm.GetSignerConfig(theContext)) assert.Nil(t, hsm.GetSignerConfig(ctx))
} }
func TestSetupModeOption(t *testing.T) { func TestSetupModeOption(t *testing.T) {
theContext := hsm.SetupContext(hsm.SetupModeOption()) ctx := hsm.SetupContext(hsm.SetupModeOption())
assert.True(t, hsm.IsSetupMode(theContext)) assert.True(t, hsm.IsSetupMode(ctx))
} }
func TestIsSetupMode_not_set(t *testing.T) { func TestIsSetupMode_not_set(t *testing.T) {
theContext := hsm.SetupContext() ctx := hsm.SetupContext()
assert.False(t, hsm.IsSetupMode(theContext)) assert.False(t, hsm.IsSetupMode(ctx))
} }
func TestVerboseLoggingOption(t *testing.T) { func TestVerboseLoggingOption(t *testing.T) {
theContext := hsm.SetupContext(hsm.VerboseLoggingOption()) ctx := hsm.SetupContext(hsm.VerboseLoggingOption())
assert.True(t, hsm.IsVerbose(theContext)) assert.True(t, hsm.IsVerbose(ctx))
} }
func TestIsVerbose_not_set(t *testing.T) { func TestIsVerbose_not_set(t *testing.T) {
theContext := hsm.SetupContext() ctx := hsm.SetupContext()
assert.False(t, hsm.IsVerbose(theContext)) assert.False(t, hsm.IsVerbose(ctx))
} }
func TestSetupContext(t *testing.T) { func TestSetupContext(t *testing.T) {
testConfig := setupSignerConfig(t) testConfig := setupSignerConfig(t)
theContext := hsm.SetupContext(hsm.SetupModeOption(), hsm.VerboseLoggingOption(), hsm.CaConfigOption(testConfig)) ctx := hsm.SetupContext(hsm.SetupModeOption(), hsm.VerboseLoggingOption(), hsm.CaConfigOption(testConfig))
assert.True(t, hsm.IsSetupMode(theContext)) assert.True(t, hsm.IsSetupMode(ctx))
assert.True(t, hsm.IsVerbose(theContext)) assert.True(t, hsm.IsVerbose(ctx))
assert.Equal(t, hsm.GetSignerConfig(theContext), testConfig) assert.Equal(t, hsm.GetSignerConfig(ctx), testConfig)
} }
func TestGetP11Context_missing_SetupContext(t *testing.T) { func TestGetP11Context_missing_SetupContext(t *testing.T) {
@ -92,33 +92,72 @@ func TestGetP11Context_missing_SetupContext(t *testing.T) {
func TestGetP11Context_unknown_storage(t *testing.T) { func TestGetP11Context_unknown_storage(t *testing.T) {
testConfig := setupSignerConfig(t) testConfig := setupSignerConfig(t)
theContext := hsm.SetupContext(hsm.SetupModeOption(), hsm.CaConfigOption(testConfig)) ctx := hsm.SetupContext(hsm.SetupModeOption(), hsm.CaConfigOption(testConfig))
definition := &config.CaCertificateEntry{Storage: "undefined"} definition := &config.CaCertificateEntry{Storage: "undefined"}
p11Context, err := hsm.GetP11Context(theContext, definition) p11Context, err := hsm.GetP11Context(ctx, definition)
assert.Error(t, err) assert.Error(t, err)
assert.ErrorContains(t, err, "key storage undefined not available") assert.ErrorContains(t, err, "key storage undefined not available")
assert.Nil(t, p11Context) assert.Nil(t, p11Context)
} }
func TestGetP11Context(t *testing.T) { func TestGetP11Context_wrong_pin(t *testing.T) {
testConfig := setupSignerConfig(t) testConfig := setupSignerConfig(t)
setupSoftHsm(t) setupSoftHsm(t)
theContext := hsm.SetupContext(hsm.CaConfigOption(testConfig)) t.Setenv("TOKEN_PIN_ACME_TEST_HSM", "wrongpin")
ctx := hsm.SetupContext(hsm.CaConfigOption(testConfig))
definition, err := testConfig.GetCADefinition("root") definition, err := testConfig.GetCADefinition("root")
require.NoError(t, err) require.NoError(t, err)
p11Context1, err := hsm.GetP11Context(theContext, definition) _, err = hsm.GetP11Context(ctx, definition)
assert.ErrorContains(t, err, "could not configure PKCS#11 library")
}
func TestGetP11Context_no_pin(t *testing.T) {
testConfig := setupSignerConfig(t)
setupSoftHsm(t)
ctx := hsm.SetupContext(hsm.CaConfigOption(testConfig))
definition, err := testConfig.GetCADefinition("root")
require.NoError(t, err)
_, err = hsm.GetP11Context(ctx, definition)
assert.ErrorContains(t, err, "stdin is not a terminal")
}
func TestGetP11Context(t *testing.T) {
testConfig := setupSignerConfig(t)
setupSoftHsm(t)
t.Setenv("TOKEN_PIN_ACME_TEST_HSM", "123456")
ctx := hsm.SetupContext(hsm.CaConfigOption(testConfig))
definition, err := testConfig.GetCADefinition("root")
require.NoError(t, err)
p11Context1, err := hsm.GetP11Context(ctx, definition)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, p11Context1) assert.NotNil(t, p11Context1)
p11Context2, err := hsm.GetP11Context(theContext, definition) p11Context2, err := hsm.GetP11Context(ctx, definition)
t.Cleanup(func() {
err := hsm.CloseP11Contexts(ctx)
assert.NoError(t, err)
})
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, p11Context1) assert.NotNil(t, p11Context1)
@ -201,6 +240,11 @@ func setupSoftHsm(t *testing.T) {
).Run() ).Run()
require.NoError(t, err) require.NoError(t, err)
}
t.Setenv("TOKEN_PIN_ACME_TEST_HSM", "123456")
func TestCloseP11Contexts_without_setup(t *testing.T) {
ctx := context.Background()
err := hsm.CloseP11Contexts(ctx)
assert.ErrorContains(t, err, "type assertion failed, use hsm.SetupContext first")
} }

View file

@ -32,6 +32,7 @@ import (
"log" "log"
"math/big" "math/big"
"os" "os"
"path"
"syscall" "syscall"
"time" "time"
@ -48,6 +49,83 @@ var (
errCertificateGenerationRefused = errors.New("not in setup mode, refusing certificate generation") errCertificateGenerationRefused = errors.New("not in setup mode, refusing certificate generation")
) )
type caFile struct {
sc *config.SignerConfig
label string
}
func (c *caFile) buildCertificatePath(ctx context.Context) (string, error) {
fileName := c.sc.CertificateFileName(c.label)
caDir := ctx.Value(ctxCADirectory)
if caDir != nil {
caPath, ok := caDir.(string)
if !ok {
return "", errors.New("context object CA directory is not a string")
}
return path.Join(caPath, fileName), nil
}
return fileName, nil
}
func (c *caFile) loadCertificate(ctx context.Context) (*x509.Certificate, error) {
certFile, err := c.buildCertificatePath(ctx)
if err != nil {
return nil, err
}
certFileInfo, err := os.Stat(certFile)
if err != nil {
if errors.Is(err, syscall.ENOENT) {
return nil, nil
}
return nil, fmt.Errorf("could not get info for %s: %w", certFile, err)
}
if !certFileInfo.Mode().IsRegular() {
return nil, fmt.Errorf("certificate file %s is not a regular file", certFile)
}
certData, err := os.ReadFile(certFile)
if err != nil {
return nil, fmt.Errorf("could not read %s: %w", certFile, err)
}
pemData, _ := pem.Decode(certData)
if pemData == nil {
return nil, fmt.Errorf("no PEM data in %s", certFile)
}
if pemData.Type != "CERTIFICATE" {
return nil, fmt.Errorf("no certificate found in %s", certFile)
}
certificate, err := x509.ParseCertificate(pemData.Bytes)
if err != nil {
return nil, fmt.Errorf("could not parse certificate from %s: %w", certFile, err)
}
return certificate, nil
}
func (c *caFile) storeCertificate(ctx context.Context, certificate []byte) error {
certFile, err := c.buildCertificatePath(ctx)
if err != nil {
return err
}
err = os.WriteFile(certFile, certificate, 0o600)
if err != nil {
return fmt.Errorf("could not write certificate file %s: %w", certFile, err)
}
return nil
}
func GetRootCACertificate(ctx context.Context, label string) (*x509.Certificate, error) { func GetRootCACertificate(ctx context.Context, label string) (*x509.Certificate, error) {
var ( var (
certificate *x509.Certificate certificate *x509.Certificate
@ -65,9 +143,9 @@ func GetRootCACertificate(ctx context.Context, label string) (*x509.Certificate,
return nil, fmt.Errorf("CA definition %s is not a root CA definition", label) return nil, fmt.Errorf("CA definition %s is not a root CA definition", label)
} }
certFile := sc.CertificateFileName(label) caFile := &caFile{sc: sc, label: label}
certificate, err = loadCertificate(certFile) certificate, err = caFile.loadCertificate(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -97,7 +175,8 @@ func GetRootCACertificate(ctx context.Context, label string) (*x509.Certificate,
subject := sc.CalculateSubject(caCert) subject := sc.CalculateSubject(caCert)
certificate, err = generateRootCACertificate( certificate, err = generateRootCACertificate(
certFile, ctx,
caFile,
keyPair, keyPair,
&x509.Certificate{ &x509.Certificate{
Subject: subject, Subject: subject,
@ -155,9 +234,9 @@ func GetIntermediaryCACertificate(ctx context.Context, certLabel string) (*x509.
return nil, err return nil, err
} }
certFile := sc.CertificateFileName(certLabel) certFile := &caFile{sc: sc, label: certLabel}
certificate, err = loadCertificate(certFile) certificate, err = certFile.loadCertificate(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -176,6 +255,8 @@ func GetIntermediaryCACertificate(ctx context.Context, certLabel string) (*x509.
subject := sc.CalculateSubject(caCert) subject := sc.CalculateSubject(caCert)
certificate, err = generateIntermediaryCACertificate( certificate, err = generateIntermediaryCACertificate(
ctx,
certFile,
sc, sc,
certLabel, certLabel,
keyPair.Public(), keyPair.Public(),
@ -219,6 +300,8 @@ func GetIntermediaryCACertificate(ctx context.Context, certLabel string) (*x509.
} }
func generateIntermediaryCACertificate( func generateIntermediaryCACertificate(
ctx context.Context,
certFile *caFile,
config *config.SignerConfig, config *config.SignerConfig,
certLabel string, certLabel string,
publicKey crypto.PublicKey, publicKey crypto.PublicKey,
@ -257,11 +340,9 @@ func generateIntermediaryCACertificate(
Bytes: certBytes, Bytes: certBytes,
} }
certFile := config.CertificateFileName(certLabel) err = certFile.storeCertificate(ctx, pem.EncodeToMemory(certBlock))
err = os.WriteFile(certFile, pem.EncodeToMemory(certBlock), 0o600)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not write certificate to %s: %w", certFile, err) return nil, err
} }
certificate, err := x509.ParseCertificate(certBytes) certificate, err := x509.ParseCertificate(certBytes)
@ -400,7 +481,8 @@ func randomObjectID() ([]byte, error) {
} }
func generateRootCACertificate( func generateRootCACertificate(
certFile string, ctx context.Context,
certFile *caFile,
keyPair crypto.Signer, keyPair crypto.Signer,
template *x509.Certificate, template *x509.Certificate,
) (*x509.Certificate, error) { ) (*x509.Certificate, error) {
@ -432,9 +514,8 @@ func generateRootCACertificate(
Bytes: certBytes, Bytes: certBytes,
} }
err = os.WriteFile(certFile, pem.EncodeToMemory(certBlock), 0o600) if err = certFile.storeCertificate(ctx, pem.EncodeToMemory(certBlock)); err != nil {
if err != nil { return nil, err
return nil, fmt.Errorf("could not write certificate to %s: %w", certFile, err)
} }
certificate, err := x509.ParseCertificate(certBytes) certificate, err := x509.ParseCertificate(certBytes)
@ -484,42 +565,6 @@ func certificateMatches(certificate *x509.Certificate, key crypto.Signer) bool {
return false return false
} }
func loadCertificate(certFile string) (*x509.Certificate, error) {
certFileInfo, err := os.Stat(certFile)
if err != nil {
if errors.Is(err, syscall.ENOENT) {
return nil, nil
}
return nil, fmt.Errorf("could not get info for %s: %w", certFile, err)
}
if !certFileInfo.Mode().IsRegular() {
return nil, fmt.Errorf("certificate file %s is not a regular file", certFile)
}
certData, err := os.ReadFile(certFile)
if err != nil {
return nil, fmt.Errorf("could not read %s: %w", certFile, err)
}
pemData, _ := pem.Decode(certData)
if pemData == nil {
return nil, fmt.Errorf("no PEM data in %s", certFile)
}
if pemData.Type != "CERTIFICATE" {
return nil, fmt.Errorf("no certificate found in %s", certFile)
}
certificate, err := x509.ParseCertificate(pemData.Bytes)
if err != nil {
return nil, fmt.Errorf("could not parse certificate from %s: %w", certFile, err)
}
return certificate, nil
}
func randomSerialNumber() (*big.Int, error) { func randomSerialNumber() (*big.Int, error) {
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil { if err != nil {

196
pkg/hsm/hsm_test.go Normal file
View file

@ -0,0 +1,196 @@
/*
Copyright 2022 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package hsm_test
import (
"context"
"crypto/x509"
"strings"
"testing"
"git.cacert.org/cacert-gosigner/pkg/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"git.cacert.org/cacert-gosigner/pkg/hsm"
)
func TestEnsureCAKeysAndCertificates_not_in_setup_mode(t *testing.T) {
testConfig := setupSignerConfig(t)
setupSoftHsm(t)
t.Setenv("TOKEN_PIN_ACME_TEST_HSM", "123456")
ctx := hsm.SetupContext(
hsm.CaConfigOption(testConfig),
hsm.CADirectoryOption(t.TempDir()))
err := hsm.EnsureCAKeysAndCertificates(ctx)
t.Cleanup(func() {
err := hsm.CloseP11Contexts(ctx)
assert.NoError(t, err)
})
assert.ErrorContains(t, err, "not in setup mode")
}
func prepareSoftHSM(t *testing.T) context.Context {
t.Helper()
testConfig := setupSignerConfig(t)
setupSoftHsm(t)
t.Setenv("TOKEN_PIN_ACME_TEST_HSM", "123456")
ctx := hsm.SetupContext(
hsm.CaConfigOption(testConfig),
hsm.SetupModeOption(),
hsm.CADirectoryOption(t.TempDir()))
err := hsm.EnsureCAKeysAndCertificates(ctx)
t.Cleanup(func() {
err := hsm.CloseP11Contexts(ctx)
assert.NoError(t, err)
})
require.NoError(t, err)
return ctx
}
func TestGetRootCACertificate(t *testing.T) {
ctx := prepareSoftHSM(t)
testData := map[string]struct {
label, errMsg string
}{
"known root": {
label: "root",
},
"unknown root": {
label: "unknown",
errMsg: "could not get CA definition for label unknown",
},
"known intermediary": {
label: "sub1",
errMsg: "CA definition sub1 is not a root CA definition",
},
}
for name, item := range testData {
t.Run(name, func(t *testing.T) {
root, err := hsm.GetRootCACertificate(ctx, item.label)
if item.errMsg != "" {
assert.ErrorContains(t, err, item.errMsg)
assert.Nil(t, root)
} else {
assert.NoError(t, err)
assert.NotNil(t, root)
}
})
}
}
func TestGetIntermediaryCACertificate(t *testing.T) {
ctx := prepareSoftHSM(t)
testData := map[string]struct {
label, errMsg string
}{
"known intermediary": {
label: "sub1",
},
"unknown intermediary": {
label: "unknown",
errMsg: "could not get CA definition for label unknown",
},
"known root": {
label: "root",
errMsg: "CA definition root is a root CA definition, intermediary expected",
},
}
for name, item := range testData {
t.Run(name, func(t *testing.T) {
root, err := hsm.GetIntermediaryCACertificate(ctx, item.label)
if item.errMsg != "" {
assert.ErrorContains(t, err, item.errMsg)
assert.Nil(t, root)
} else {
assert.NoError(t, err)
assert.NotNil(t, root)
}
})
}
}
func TestRSAKeyGeneration(t *testing.T) {
const testRSASignerConfig = `---
Settings:
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 30
intermediary: 10
url-patterns:
ocsp: http://ocsp.example.org/
crl: http://crl.example.org/%s.crl
issuer: http://%s.cas.example.org/
CAs:
root:
common-name: "Acme CAs root"
key-info:
algorithm: RSA
rsa-bits: 2048
KeyStorage:
default:
type: softhsm
label: acme-test-hsm
`
testConfig, err := config.LoadConfiguration(strings.NewReader(testRSASignerConfig))
require.NoError(t, err)
setupSoftHsm(t)
t.Setenv("TOKEN_PIN_ACME_TEST_HSM", "123456")
ctx := hsm.SetupContext(
hsm.CaConfigOption(testConfig),
hsm.SetupModeOption(),
hsm.CADirectoryOption(t.TempDir()))
err = hsm.EnsureCAKeysAndCertificates(ctx)
t.Cleanup(func() {
err := hsm.CloseP11Contexts(ctx)
assert.NoError(t, err)
})
require.NoError(t, err)
root, err := hsm.GetRootCACertificate(ctx, "root")
assert.NoError(t, err)
assert.NotNil(t, root)
assert.Equal(t, x509.RSA, root.PublicKeyAlgorithm)
}

View file

@ -27,7 +27,7 @@ func EnsureCAKeysAndCertificates(ctx context.Context) error {
conf := GetSignerConfig(ctx) conf := GetSignerConfig(ctx)
for _, label := range conf.RootCAs() { for _, label = range conf.RootCAs() {
crt, err := GetRootCACertificate(ctx, label) crt, err := GetRootCACertificate(ctx, label)
if err != nil { if err != nil {
return err return err

89
pkg/hsm/setup_test.go Normal file
View file

@ -0,0 +1,89 @@
/*
Copyright 2022 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package hsm_test
import (
"bytes"
"log"
"testing"
"github.com/stretchr/testify/assert"
"git.cacert.org/cacert-gosigner/pkg/hsm"
)
func TestEnsureCAKeysAndCertificates(t *testing.T) {
testConfig := setupSignerConfig(t)
setupSoftHsm(t)
t.Setenv("TOKEN_PIN_ACME_TEST_HSM", "123456")
buf := bytes.NewBuffer(nil)
log.SetOutput(buf)
ctx := hsm.SetupContext(
hsm.CaConfigOption(testConfig),
hsm.SetupModeOption(),
hsm.CADirectoryOption(t.TempDir()))
err := hsm.EnsureCAKeysAndCertificates(ctx)
t.Cleanup(func() {
err := hsm.CloseP11Contexts(ctx)
assert.NoError(t, err)
})
assert.NoError(t, err)
output := buf.String()
assert.NoError(t, err)
assert.Contains(t, output, "found root CA certificate root: Acme CAs root")
assert.Contains(t, output, "found intermediary CA certificate sub1: Acme CAs server sub CA")
}
func TestEnsureCAKeysAndCertificates_verbose(t *testing.T) {
testConfig := setupSignerConfig(t)
setupSoftHsm(t)
t.Setenv("TOKEN_PIN_ACME_TEST_HSM", "123456")
buf := bytes.NewBuffer(nil)
log.SetOutput(buf)
ctx := hsm.SetupContext(
hsm.CaConfigOption(testConfig),
hsm.SetupModeOption(),
hsm.VerboseLoggingOption(),
hsm.CADirectoryOption(t.TempDir()))
err := hsm.EnsureCAKeysAndCertificates(ctx)
t.Cleanup(func() {
err := hsm.CloseP11Contexts(ctx)
assert.NoError(t, err)
})
output := buf.String()
assert.NoError(t, err)
assert.Contains(t, output, "found root CA certificate root:\n Subject")
assert.Contains(t, output, "found intermediary CA certificate sub1:\n Subject")
}