2022-09-21 19:26:45 +00:00
|
|
|
//go:build !go1.18
|
|
|
|
// +build !go1.18
|
|
|
|
|
2019-05-27 00:41:10 +00:00
|
|
|
package provisioner
|
|
|
|
|
|
|
|
import (
|
2022-09-15 22:50:04 +00:00
|
|
|
"bytes"
|
2019-05-27 00:41:10 +00:00
|
|
|
"context"
|
|
|
|
"crypto/x509"
|
2022-03-30 08:22:22 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2019-12-20 21:30:05 +00:00
|
|
|
"net/http"
|
2022-09-15 22:50:04 +00:00
|
|
|
"os"
|
2019-05-27 00:41:10 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/smallstep/assert"
|
2022-03-30 08:22:22 +00:00
|
|
|
"github.com/smallstep/certificates/api/render"
|
2019-05-27 00:41:10 +00:00
|
|
|
)
|
|
|
|
|
2022-09-08 19:34:06 +00:00
|
|
|
func TestACMEChallenge_Validate(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
c ACMEChallenge
|
|
|
|
wantErr bool
|
|
|
|
}{
|
|
|
|
{"http-01", HTTP_01, false},
|
|
|
|
{"dns-01", DNS_01, false},
|
|
|
|
{"tls-alpn-01", TLS_ALPN_01, false},
|
|
|
|
{"device-attest-01", DEVICE_ATTEST_01, false},
|
|
|
|
{"uppercase", "HTTP-01", false},
|
|
|
|
{"fail", "http-02", true},
|
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
if err := tt.c.Validate(); (err != nil) != tt.wantErr {
|
|
|
|
t.Errorf("ACMEChallenge.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-09 00:16:50 +00:00
|
|
|
func TestACMEAttestationFormat_Validate(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
f ACMEAttestationFormat
|
|
|
|
wantErr bool
|
|
|
|
}{
|
|
|
|
{"apple", APPLE, false},
|
|
|
|
{"step", STEP, false},
|
|
|
|
{"tpm", TPM, false},
|
|
|
|
{"uppercase", "APPLE", false},
|
|
|
|
{"fail", "FOO", true},
|
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
if err := tt.f.Validate(); (err != nil) != tt.wantErr {
|
|
|
|
t.Errorf("ACMEAttestationFormat.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-27 00:41:10 +00:00
|
|
|
func TestACME_Getters(t *testing.T) {
|
|
|
|
p, err := generateACME()
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
id := "acme/" + p.Name
|
|
|
|
if got := p.GetID(); got != id {
|
|
|
|
t.Errorf("ACME.GetID() = %v, want %v", got, id)
|
|
|
|
}
|
|
|
|
if got := p.GetName(); got != p.Name {
|
|
|
|
t.Errorf("ACME.GetName() = %v, want %v", got, p.Name)
|
|
|
|
}
|
|
|
|
if got := p.GetType(); got != TypeACME {
|
|
|
|
t.Errorf("ACME.GetType() = %v, want %v", got, TypeACME)
|
|
|
|
}
|
|
|
|
kid, key, ok := p.GetEncryptedKey()
|
|
|
|
if kid != "" || key != "" || ok == true {
|
|
|
|
t.Errorf("ACME.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)",
|
|
|
|
kid, key, ok, "", "", false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestACME_Init(t *testing.T) {
|
2022-09-15 22:50:04 +00:00
|
|
|
appleCA, err := os.ReadFile("testdata/certs/apple-att-ca.crt")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
yubicoCA, err := os.ReadFile("testdata/certs/yubico-piv-ca.crt")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2019-05-27 00:41:10 +00:00
|
|
|
type ProvisionerValidateTest struct {
|
|
|
|
p *ACME
|
|
|
|
err error
|
|
|
|
}
|
|
|
|
tests := map[string]func(*testing.T) ProvisionerValidateTest{
|
|
|
|
"fail-empty": func(t *testing.T) ProvisionerValidateTest {
|
|
|
|
return ProvisionerValidateTest{
|
|
|
|
p: &ACME{},
|
|
|
|
err: errors.New("provisioner type cannot be empty"),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail-empty-name": func(t *testing.T) ProvisionerValidateTest {
|
|
|
|
return ProvisionerValidateTest{
|
|
|
|
p: &ACME{
|
|
|
|
Type: "ACME",
|
|
|
|
},
|
|
|
|
err: errors.New("provisioner name cannot be empty"),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail-empty-type": func(t *testing.T) ProvisionerValidateTest {
|
|
|
|
return ProvisionerValidateTest{
|
|
|
|
p: &ACME{Name: "foo"},
|
|
|
|
err: errors.New("provisioner type cannot be empty"),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail-bad-claims": func(t *testing.T) ProvisionerValidateTest {
|
|
|
|
return ProvisionerValidateTest{
|
|
|
|
p: &ACME{Name: "foo", Type: "bar", Claims: &Claims{DefaultTLSDur: &Duration{0}}},
|
2021-05-03 19:48:20 +00:00
|
|
|
err: errors.New("claims: MinTLSCertDuration must be greater than 0"),
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
},
|
2022-09-08 19:34:06 +00:00
|
|
|
"fail-bad-challenge": func(t *testing.T) ProvisionerValidateTest {
|
|
|
|
return ProvisionerValidateTest{
|
|
|
|
p: &ACME{Name: "foo", Type: "bar", Challenges: []ACMEChallenge{HTTP_01, "zar"}},
|
|
|
|
err: errors.New("acme challenge \"zar\" is not supported"),
|
|
|
|
}
|
|
|
|
},
|
2022-09-09 00:16:50 +00:00
|
|
|
"fail-bad-attestation-format": func(t *testing.T) ProvisionerValidateTest {
|
|
|
|
return ProvisionerValidateTest{
|
|
|
|
p: &ACME{Name: "foo", Type: "bar", AttestationFormats: []ACMEAttestationFormat{APPLE, "zar"}},
|
|
|
|
err: errors.New("acme attestation format \"zar\" is not supported"),
|
|
|
|
}
|
|
|
|
},
|
2022-09-15 22:50:04 +00:00
|
|
|
"fail-parse-attestation-roots": func(t *testing.T) ProvisionerValidateTest {
|
|
|
|
return ProvisionerValidateTest{
|
|
|
|
p: &ACME{Name: "foo", Type: "bar", AttestationRoots: []byte("-----BEGIN CERTIFICATE-----\nZm9v\n-----END CERTIFICATE-----")},
|
|
|
|
err: errors.New("error parsing attestationRoots: malformed certificate"),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"fail-empty-attestation-roots": func(t *testing.T) ProvisionerValidateTest {
|
|
|
|
return ProvisionerValidateTest{
|
|
|
|
p: &ACME{Name: "foo", Type: "bar", AttestationRoots: []byte("\n")},
|
|
|
|
err: errors.New("error parsing attestationRoots: no certificates found"),
|
|
|
|
}
|
|
|
|
},
|
2019-05-27 00:41:10 +00:00
|
|
|
"ok": func(t *testing.T) ProvisionerValidateTest {
|
|
|
|
return ProvisionerValidateTest{
|
|
|
|
p: &ACME{Name: "foo", Type: "bar"},
|
|
|
|
}
|
|
|
|
},
|
2022-09-09 00:16:50 +00:00
|
|
|
"ok attestation": func(t *testing.T) ProvisionerValidateTest {
|
2022-09-08 19:34:06 +00:00
|
|
|
return ProvisionerValidateTest{
|
|
|
|
p: &ACME{
|
2022-09-09 00:16:50 +00:00
|
|
|
Name: "foo",
|
|
|
|
Type: "bar",
|
|
|
|
Challenges: []ACMEChallenge{DNS_01, DEVICE_ATTEST_01},
|
|
|
|
AttestationFormats: []ACMEAttestationFormat{APPLE, STEP},
|
2022-09-15 22:50:04 +00:00
|
|
|
AttestationRoots: bytes.Join([][]byte{appleCA, yubicoCA}, []byte("\n")),
|
2022-09-08 19:34:06 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
config := Config{
|
|
|
|
Claims: globalProvisionerClaims,
|
|
|
|
Audiences: testAudiences,
|
|
|
|
}
|
|
|
|
for name, get := range tests {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
tc := get(t)
|
2022-09-15 22:50:04 +00:00
|
|
|
t.Log(string(tc.p.AttestationRoots))
|
2019-05-27 00:41:10 +00:00
|
|
|
err := tc.p.Init(config)
|
|
|
|
if err != nil {
|
|
|
|
if assert.NotNil(t, tc.err) {
|
|
|
|
assert.Equals(t, tc.err.Error(), err.Error())
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
assert.Nil(t, tc.err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-14 18:48:06 +00:00
|
|
|
func TestACME_AuthorizeRenew(t *testing.T) {
|
2022-03-10 18:46:28 +00:00
|
|
|
now := time.Now().Truncate(time.Second)
|
2019-12-20 21:30:05 +00:00
|
|
|
type test struct {
|
|
|
|
p *ACME
|
2019-05-27 00:41:10 +00:00
|
|
|
cert *x509.Certificate
|
|
|
|
err error
|
2019-12-20 21:30:05 +00:00
|
|
|
code int
|
|
|
|
}
|
|
|
|
tests := map[string]func(*testing.T) test{
|
|
|
|
"fail/renew-disabled": func(t *testing.T) test {
|
|
|
|
p, err := generateACME()
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
// disable renewal
|
|
|
|
disable := true
|
|
|
|
p.Claims = &Claims{DisableRenewal: &disable}
|
2022-03-10 02:43:45 +00:00
|
|
|
p.ctl.Claimer, err = NewClaimer(p.Claims, globalProvisionerClaims)
|
2019-12-20 21:30:05 +00:00
|
|
|
assert.FatalError(t, err)
|
|
|
|
return test{
|
2022-03-10 02:43:45 +00:00
|
|
|
p: p,
|
|
|
|
cert: &x509.Certificate{
|
|
|
|
NotBefore: now,
|
|
|
|
NotAfter: now.Add(time.Hour),
|
|
|
|
},
|
2019-12-20 21:30:05 +00:00
|
|
|
code: http.StatusUnauthorized,
|
2022-03-30 08:22:22 +00:00
|
|
|
err: fmt.Errorf("renew is disabled for provisioner '%s'", p.GetName()),
|
2019-12-20 21:30:05 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
"ok": func(t *testing.T) test {
|
|
|
|
p, err := generateACME()
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
return test{
|
2022-03-10 02:43:45 +00:00
|
|
|
p: p,
|
|
|
|
cert: &x509.Certificate{
|
|
|
|
NotBefore: now,
|
|
|
|
NotAfter: now.Add(time.Hour),
|
|
|
|
},
|
2019-12-20 21:30:05 +00:00
|
|
|
}
|
|
|
|
},
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
2019-12-20 21:30:05 +00:00
|
|
|
for name, tt := range tests {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
tc := tt(t)
|
|
|
|
if err := tc.p.AuthorizeRenew(context.Background(), tc.cert); err != nil {
|
2022-09-21 04:48:04 +00:00
|
|
|
sc, ok := err.(render.StatusCodedError)
|
2022-03-30 08:22:22 +00:00
|
|
|
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
2019-12-20 21:30:05 +00:00
|
|
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
|
|
|
if assert.NotNil(t, tc.err) {
|
|
|
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
} else {
|
2019-12-20 21:30:05 +00:00
|
|
|
assert.Nil(t, tc.err)
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestACME_AuthorizeSign(t *testing.T) {
|
2019-12-20 21:30:05 +00:00
|
|
|
type test struct {
|
|
|
|
p *ACME
|
|
|
|
token string
|
|
|
|
code int
|
|
|
|
err error
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
2019-12-20 21:30:05 +00:00
|
|
|
tests := map[string]func(*testing.T) test{
|
|
|
|
"ok": func(t *testing.T) test {
|
|
|
|
p, err := generateACME()
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
return test{
|
|
|
|
p: p,
|
|
|
|
token: "foo",
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for name, tt := range tests {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
tc := tt(t)
|
|
|
|
if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil {
|
|
|
|
if assert.NotNil(t, tc.err) {
|
2022-09-21 04:48:04 +00:00
|
|
|
sc, ok := err.(render.StatusCodedError)
|
2022-03-30 08:22:22 +00:00
|
|
|
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
2019-12-20 21:30:05 +00:00
|
|
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
|
|
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
} else {
|
2019-12-20 21:30:05 +00:00
|
|
|
if assert.Nil(t, tc.err) && assert.NotNil(t, opts) {
|
2022-09-30 00:16:26 +00:00
|
|
|
assert.Equals(t, 8, len(opts)) // number of SignOptions returned
|
2019-12-20 21:30:05 +00:00
|
|
|
for _, o := range opts {
|
2019-09-05 01:31:09 +00:00
|
|
|
switch v := o.(type) {
|
2022-03-28 22:06:56 +00:00
|
|
|
case *ACME:
|
2019-09-05 01:31:09 +00:00
|
|
|
case *provisionerExtensionOption:
|
2022-03-11 22:59:42 +00:00
|
|
|
assert.Equals(t, v.Type, TypeACME)
|
2019-12-20 21:30:05 +00:00
|
|
|
assert.Equals(t, v.Name, tc.p.GetName())
|
2019-09-05 01:31:09 +00:00
|
|
|
assert.Equals(t, v.CredentialID, "")
|
|
|
|
assert.Len(t, 0, v.KeyValuePairs)
|
2020-05-17 17:27:09 +00:00
|
|
|
case *forceCNOption:
|
|
|
|
assert.Equals(t, v.ForceCN, tc.p.ForceCN)
|
2019-09-05 01:31:09 +00:00
|
|
|
case profileDefaultDuration:
|
2022-03-10 02:43:45 +00:00
|
|
|
assert.Equals(t, time.Duration(v), tc.p.ctl.Claimer.DefaultTLSCertDuration())
|
2019-09-05 01:31:09 +00:00
|
|
|
case defaultPublicKeyValidator:
|
|
|
|
case *validityValidator:
|
2022-03-10 02:43:45 +00:00
|
|
|
assert.Equals(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration())
|
|
|
|
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
2022-01-03 11:25:24 +00:00
|
|
|
case *x509NamePolicyValidator:
|
|
|
|
assert.Equals(t, nil, v.policyEngine)
|
2022-09-30 00:16:26 +00:00
|
|
|
case *WebhookController:
|
|
|
|
assert.Len(t, 0, v.webhooks)
|
2019-09-05 01:31:09 +00:00
|
|
|
default:
|
2022-03-30 08:22:22 +00:00
|
|
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
2019-09-05 01:31:09 +00:00
|
|
|
}
|
|
|
|
}
|
2019-05-27 00:41:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2022-08-24 19:31:09 +00:00
|
|
|
|
2022-09-08 20:22:35 +00:00
|
|
|
func TestACME_IsChallengeEnabled(t *testing.T) {
|
2022-08-24 19:31:09 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
type fields struct {
|
2022-09-08 19:34:06 +00:00
|
|
|
Challenges []ACMEChallenge
|
2022-08-24 19:31:09 +00:00
|
|
|
}
|
|
|
|
type args struct {
|
|
|
|
ctx context.Context
|
2022-09-08 19:34:06 +00:00
|
|
|
challenge ACMEChallenge
|
2022-08-24 19:31:09 +00:00
|
|
|
}
|
|
|
|
tests := []struct {
|
2022-09-08 20:22:35 +00:00
|
|
|
name string
|
|
|
|
fields fields
|
|
|
|
args args
|
|
|
|
want bool
|
2022-08-24 19:31:09 +00:00
|
|
|
}{
|
2022-09-08 20:22:35 +00:00
|
|
|
{"ok http-01", fields{nil}, args{ctx, HTTP_01}, true},
|
|
|
|
{"ok dns-01", fields{nil}, args{ctx, DNS_01}, true},
|
|
|
|
{"ok tls-alpn-01", fields{[]ACMEChallenge{}}, args{ctx, TLS_ALPN_01}, true},
|
|
|
|
{"fail device-attest-01", fields{[]ACMEChallenge{}}, args{ctx, "device-attest-01"}, false},
|
|
|
|
{"ok http-01 enabled", fields{[]ACMEChallenge{"http-01"}}, args{ctx, "HTTP-01"}, true},
|
|
|
|
{"ok dns-01 enabled", fields{[]ACMEChallenge{"http-01", "dns-01"}}, args{ctx, DNS_01}, true},
|
|
|
|
{"ok tls-alpn-01 enabled", fields{[]ACMEChallenge{"http-01", "dns-01", "tls-alpn-01"}}, args{ctx, TLS_ALPN_01}, true},
|
|
|
|
{"ok device-attest-01 enabled", fields{[]ACMEChallenge{"device-attest-01", "dns-01"}}, args{ctx, DEVICE_ATTEST_01}, true},
|
|
|
|
{"fail http-01", fields{[]ACMEChallenge{"dns-01"}}, args{ctx, "http-01"}, false},
|
|
|
|
{"fail dns-01", fields{[]ACMEChallenge{"http-01", "tls-alpn-01"}}, args{ctx, "dns-01"}, false},
|
|
|
|
{"fail tls-alpn-01", fields{[]ACMEChallenge{"http-01", "dns-01", "device-attest-01"}}, args{ctx, "tls-alpn-01"}, false},
|
|
|
|
{"fail device-attest-01", fields{[]ACMEChallenge{"http-01", "dns-01"}}, args{ctx, "device-attest-01"}, false},
|
|
|
|
{"fail unknown", fields{[]ACMEChallenge{"http-01", "dns-01", "tls-alpn-01", "device-attest-01"}}, args{ctx, "unknown"}, false},
|
2022-08-24 19:31:09 +00:00
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
p := &ACME{
|
|
|
|
Challenges: tt.fields.Challenges,
|
|
|
|
}
|
2022-09-08 20:22:35 +00:00
|
|
|
if got := p.IsChallengeEnabled(tt.args.ctx, tt.args.challenge); got != tt.want {
|
|
|
|
t.Errorf("ACME.AuthorizeChallenge() = %v, want %v", got, tt.want)
|
2022-08-24 19:31:09 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2022-09-09 00:16:50 +00:00
|
|
|
|
|
|
|
func TestACME_IsAttestationFormatEnabled(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
type fields struct {
|
|
|
|
AttestationFormats []ACMEAttestationFormat
|
|
|
|
}
|
|
|
|
type args struct {
|
|
|
|
ctx context.Context
|
|
|
|
format ACMEAttestationFormat
|
|
|
|
}
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
fields fields
|
|
|
|
args args
|
|
|
|
want bool
|
|
|
|
}{
|
|
|
|
{"ok", fields{[]ACMEAttestationFormat{APPLE, STEP, TPM}}, args{ctx, TPM}, true},
|
|
|
|
{"ok empty apple", fields{nil}, args{ctx, APPLE}, true},
|
|
|
|
{"ok empty step", fields{nil}, args{ctx, STEP}, true},
|
|
|
|
{"ok empty tpm", fields{[]ACMEAttestationFormat{}}, args{ctx, "tpm"}, true},
|
|
|
|
{"ok uppercase", fields{[]ACMEAttestationFormat{APPLE, STEP, TPM}}, args{ctx, "STEP"}, true},
|
|
|
|
{"fail apple", fields{[]ACMEAttestationFormat{STEP, TPM}}, args{ctx, APPLE}, false},
|
|
|
|
{"fail step", fields{[]ACMEAttestationFormat{APPLE, TPM}}, args{ctx, STEP}, false},
|
|
|
|
{"fail step", fields{[]ACMEAttestationFormat{APPLE, STEP}}, args{ctx, TPM}, false},
|
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
p := &ACME{
|
|
|
|
AttestationFormats: tt.fields.AttestationFormats,
|
|
|
|
}
|
|
|
|
if got := p.IsAttestationFormatEnabled(tt.args.ctx, tt.args.format); got != tt.want {
|
|
|
|
t.Errorf("ACME.IsAttestationFormatEnabled() = %v, want %v", got, tt.want)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|