2024-10-17 13:40:17 -05:00
/ *
2024-10-18 09:25:31 -05:00
Copyright Docker attest authors
2024-10-17 13:40:17 -05:00
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 .
* /
2024-10-18 09:25:31 -05:00
2024-04-29 12:52:39 -05:00
package attest
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
2024-09-18 13:34:10 +01:00
"time"
2024-04-29 12:52:39 -05:00
2024-10-16 12:01:31 -05:00
"github.com/Masterminds/semver/v3"
2024-08-21 18:01:11 +01:00
"github.com/distribution/reference"
2024-09-02 16:17:50 +01:00
"github.com/docker/attest/attestation"
2024-05-22 14:49:23 +01:00
"github.com/docker/attest/internal/test"
2024-09-18 21:11:55 +01:00
"github.com/docker/attest/mapping"
2024-09-02 16:17:50 +01:00
"github.com/docker/attest/oci"
"github.com/docker/attest/policy"
2024-09-18 13:34:10 +01:00
"github.com/docker/attest/tlog"
2024-09-02 16:17:50 +01:00
"github.com/docker/attest/tuf"
2024-10-16 12:01:31 -05:00
"github.com/docker/attest/version"
2024-05-22 14:49:23 +01:00
intoto "github.com/in-toto/in-toto-golang/in_toto"
2024-09-18 13:34:10 +01:00
"github.com/secure-systems-lab/go-securesystemslib/dsse"
2024-04-29 12:52:39 -05:00
"github.com/stretchr/testify/assert"
2024-05-22 14:49:23 +01:00
"github.com/stretchr/testify/require"
2024-09-05 14:08:55 +01:00
"sigs.k8s.io/yaml"
2024-04-29 12:52:39 -05:00
)
2024-09-05 14:08:55 +01:00
var (
ExampleAttestation = filepath . Join ( "test" , "testdata" , "example_attestation.json" )
LocalKeysPolicy = filepath . Join ( "test" , "testdata" , "local-policy-real" )
2024-10-15 16:07:26 +01:00
LocalParamPolicy = filepath . Join ( "test" , "testdata" , "local-policy-param" )
ExpiresPolicy = filepath . Join ( "test" , "testdata" , "expires" )
2024-09-05 14:08:55 +01:00
)
2024-04-29 12:52:39 -05:00
2024-06-21 11:29:16 +01:00
const (
2024-10-16 12:01:31 -05:00
LinuxAMD64 = "linux/amd64"
TestVerifierVersion = "9.9.9"
2024-06-21 11:29:16 +01:00
)
2024-10-16 12:01:31 -05:00
type MockVersionFetcher struct { }
func ( m * MockVersionFetcher ) Get ( ) ( * semver . Version , error ) {
return semver . NewVersion ( TestVerifierVersion )
}
2024-04-29 12:52:39 -05:00
func TestVerifyAttestations ( t * testing . T ) {
ex , err := os . ReadFile ( ExampleAttestation )
assert . NoError ( t , err )
2024-10-07 13:34:04 -05:00
env := new ( attestation . EnvelopeReference )
2024-04-29 12:52:39 -05:00
err = json . Unmarshal ( ex , env )
assert . NoError ( t , err )
2024-08-12 14:49:52 -05:00
resolver := & attestation . MockResolver {
2024-10-07 13:34:04 -05:00
Envs : [ ] * attestation . EnvelopeReference { env } ,
2024-04-29 12:52:39 -05:00
}
testCases := [ ] struct {
name string
policyEvaluationError error
expectedError error
} {
{ "policy ok" , nil , nil } ,
{ "policy error" , fmt . Errorf ( "policy error" ) , fmt . Errorf ( "policy evaluation failed: policy error" ) } ,
}
2024-08-29 17:43:45 +01:00
ctx := context . Background ( )
2024-04-29 12:52:39 -05:00
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
2024-05-02 14:46:21 +01:00
mockPE := policy . MockPolicyEvaluator {
2024-08-12 14:49:52 -05:00
EvaluateFunc : func ( _ context . Context , _ attestation . Resolver , _ * policy . Policy , _ * policy . Input ) ( * policy . Result , error ) {
2024-05-02 14:46:21 +01:00
return policy . AllowedResult ( ) , tc . policyEvaluationError
2024-04-29 12:52:39 -05:00
} ,
}
2024-10-16 12:01:31 -05:00
verifier , err := NewImageVerifier ( ctx , & policy . Options { } )
require . NoError ( t , err )
_ , err = verifier . verifyAttestations ( ctx , resolver , & mockPE , & policy . Policy { ResolvedName : "" } )
2024-04-29 12:52:39 -05:00
if tc . expectedError != nil {
2024-05-22 14:49:23 +01:00
if assert . Error ( t , err ) {
assert . Equal ( t , tc . expectedError . Error ( ) , err . Error ( ) )
}
2024-04-29 12:52:39 -05:00
} else {
assert . NoError ( t , err )
}
} )
}
}
2024-05-22 14:49:23 +01:00
func TestVSA ( t * testing . T ) {
ctx , signer := test . Setup ( t )
// setup an image with signed attestations
outputLayout := test . CreateTempDir ( t , "" , TestTempDir )
2024-09-18 13:34:10 +01:00
opts := & attestation . SigningOptions {
TransparencyLog : tlog . GetMockTL ( ) ,
}
2024-09-30 20:53:13 +01:00
attIdx , err := oci . IndexFromPath ( test . UnsignedTestIndex ( ) )
2024-05-22 14:49:23 +01:00
assert . NoError ( t , err )
2024-07-05 09:29:14 +01:00
signedManifests , err := SignStatements ( ctx , attIdx . Index , signer , opts )
require . NoError ( t , err )
signedIndex := attIdx . Index
2024-07-16 10:05:17 +01:00
signedIndex , err = attestation . UpdateIndexImages ( signedIndex , signedManifests )
2024-07-05 09:29:14 +01:00
require . NoError ( t , err )
2024-05-22 14:49:23 +01:00
// output signed attestations
2024-08-05 13:24:58 -05:00
spec , err := oci . ParseImageSpec ( oci . LocalPrefix + outputLayout , oci . WithPlatform ( LinuxAMD64 ) )
require . NoError ( t , err )
2024-09-09 14:22:17 +01:00
err = oci . SaveIndex ( ctx , [ ] * oci . ImageSpec { spec } , signedIndex , attIdx . Name )
2024-05-22 14:49:23 +01:00
assert . NoError ( t , err )
// mocked vsa query should pass
2024-08-01 15:35:15 +01:00
policyOpts := & policy . Options {
2024-08-05 13:24:58 -05:00
LocalPolicyDir : PassPolicyDir ,
2024-09-18 21:11:55 +01:00
AttestationStyle : mapping . AttestationStyleAttached ,
2024-08-28 09:53:52 +01:00
DisableTUF : true ,
2024-05-22 14:49:23 +01:00
}
2024-10-16 12:01:31 -05:00
verifier , err := NewImageVerifier ( ctx , policyOpts )
require . NoError ( t , err )
verifier . versionFetcher = & MockVersionFetcher { }
results , err := verifier . Verify ( ctx , spec )
2024-05-22 14:49:23 +01:00
require . NoError ( t , err )
assert . Equal ( t , OutcomeSuccess , results . Outcome )
assert . Empty ( t , results . Violations )
2024-06-17 17:28:22 +01:00
if assert . NotNil ( t , results . Input ) {
2024-09-30 20:53:13 +01:00
assert . Equal ( t , test . UnsignedLinuxAMD64ImageDigest , results . Input . Digest )
2024-08-21 18:01:11 +01:00
assert . NotNil ( t , results . Input . Tag )
2024-06-17 17:28:22 +01:00
}
2024-05-22 14:49:23 +01:00
assert . Equal ( t , intoto . StatementInTotoV01 , results . VSA . Type )
assert . Equal ( t , attestation . VSAPredicateType , results . VSA . PredicateType )
assert . Len ( t , results . VSA . Subject , 1 )
require . IsType ( t , attestation . VSAPredicate { } , results . VSA . Predicate )
2024-08-01 15:35:15 +01:00
attestationPredicate , ok := results . VSA . Predicate . ( attestation . VSAPredicate )
require . True ( t , ok )
2024-05-22 14:49:23 +01:00
assert . Equal ( t , "PASSED" , attestationPredicate . VerificationResult )
assert . Equal ( t , "docker-official-images" , attestationPredicate . Verifier . ID )
assert . Equal ( t , [ ] string { "SLSA_BUILD_LEVEL_3" } , attestationPredicate . VerifiedLevels )
2024-08-14 12:32:51 -05:00
assert . Equal ( t , PassPolicyDir + "/policy.rego" , attestationPredicate . Policy . DownloadLocation )
assert . Equal ( t , "https://docker.com/official/policy/v0.1" , attestationPredicate . Policy . URI )
2024-09-18 13:34:10 +01:00
// this is the digest of the policy file
2024-10-07 13:36:30 -05:00
assert . Equal ( t , map [ string ] string { "sha256" : "fe1d4973f3521009a3adec206946e12aae935a2aceeb1e01f52b5d4cb9de79a5" } , attestationPredicate . Policy . Digest )
assert . Greater ( t , len ( attestationPredicate . InputAttestations ) , 0 )
for _ , input := range attestationPredicate . InputAttestations {
require . NotEmpty ( t , input . Digest )
digest , ok := input . Digest [ "sha256" ]
assert . True ( t , ok )
assert . NotEmpty ( t , digest )
assert . Contains ( t , [ ] string { "application/vnd.in-toto.provenance+dsse" , "application/vnd.in-toto.spdx+dsse" } , input . MediaType )
}
2024-10-16 12:01:31 -05:00
assert . Equal ( t , TestVerifierVersion , attestationPredicate . Verifier . Version [ version . ThisModulePath ] )
2024-05-22 14:49:23 +01:00
}
func TestVerificationFailure ( t * testing . T ) {
ctx , signer := test . Setup ( t )
// setup an image with signed attestations
outputLayout := test . CreateTempDir ( t , "" , TestTempDir )
2024-09-18 13:34:10 +01:00
opts := & attestation . SigningOptions {
TransparencyLog : tlog . GetMockTL ( ) ,
}
2024-09-30 20:53:13 +01:00
attIdx , err := oci . IndexFromPath ( test . UnsignedTestIndex ( ) )
2024-05-22 14:49:23 +01:00
assert . NoError ( t , err )
2024-07-05 09:29:14 +01:00
signedManifests , err := SignStatements ( ctx , attIdx . Index , signer , opts )
require . NoError ( t , err )
signedIndex := attIdx . Index
2024-07-16 10:05:17 +01:00
signedIndex , err = attestation . UpdateIndexImages ( signedIndex , signedManifests , attestation . WithReplacedLayers ( true ) )
2024-07-05 09:29:14 +01:00
require . NoError ( t , err )
2024-05-22 14:49:23 +01:00
// output signed attestations
2024-08-05 13:24:58 -05:00
spec , err := oci . ParseImageSpec ( oci . LocalPrefix + outputLayout , oci . WithPlatform ( LinuxAMD64 ) )
require . NoError ( t , err )
2024-09-09 14:22:17 +01:00
err = oci . SaveIndex ( ctx , [ ] * oci . ImageSpec { spec } , signedIndex , attIdx . Name )
2024-05-22 14:49:23 +01:00
assert . NoError ( t , err )
2024-06-06 09:59:32 +01:00
// mocked vsa query should fail
2024-08-01 15:35:15 +01:00
policyOpts := & policy . Options {
2024-08-05 13:24:58 -05:00
LocalPolicyDir : FailPolicyDir ,
2024-09-18 21:11:55 +01:00
AttestationStyle : mapping . AttestationStyleAttached ,
2024-08-28 09:53:52 +01:00
DisableTUF : true ,
2024-05-22 14:49:23 +01:00
}
2024-08-05 13:24:58 -05:00
results , err := Verify ( ctx , spec , policyOpts )
2024-05-22 14:49:23 +01:00
require . NoError ( t , err )
assert . Equal ( t , OutcomeFailure , results . Outcome )
assert . Len ( t , results . Violations , 1 )
violation := results . Violations [ 0 ]
assert . Equal ( t , "missing_attestation" , violation . Type )
assert . Equal ( t , "Attestation missing for subject" , violation . Description )
assert . Nil ( t , violation . Attestation )
assert . Equal ( t , intoto . StatementInTotoV01 , results . VSA . Type )
assert . Equal ( t , attestation . VSAPredicateType , results . VSA . PredicateType )
assert . Len ( t , results . VSA . Subject , 1 )
require . IsType ( t , attestation . VSAPredicate { } , results . VSA . Predicate )
2024-08-01 15:35:15 +01:00
attestationPredicate , ok := results . VSA . Predicate . ( attestation . VSAPredicate )
require . True ( t , ok )
2024-05-22 14:49:23 +01:00
assert . Equal ( t , "FAILED" , attestationPredicate . VerificationResult )
assert . Equal ( t , "docker-official-images" , attestationPredicate . Verifier . ID )
assert . Equal ( t , [ ] string { "SLSA_BUILD_LEVEL_3" } , attestationPredicate . VerifiedLevels )
2024-08-14 12:32:51 -05:00
assert . Equal ( t , FailPolicyDir + "/policy.rego" , attestationPredicate . Policy . DownloadLocation )
assert . Equal ( t , "https://docker.com/official/policy/v0.1" , attestationPredicate . Policy . URI )
2024-09-18 13:34:10 +01:00
assert . Equal ( t , map [ string ] string { "sha256" : "4345a4f5db3ce02664bd83f8e4aad03bd9a26d4edb334338c762d9648e16bed1" } , attestationPredicate . Policy . Digest )
2024-05-22 14:49:23 +01:00
}
2024-06-06 09:59:32 +01:00
2024-07-12 17:09:41 +01:00
func TestSignVerify ( t * testing . T ) {
2024-06-06 09:59:32 +01:00
ctx , signer := test . Setup ( t )
// setup an image with signed attestations
outputLayout := test . CreateTempDir ( t , "" , TestTempDir )
2024-09-18 13:34:10 +01:00
keys , err := GenKeyMetadata ( signer )
2024-09-05 14:08:55 +01:00
require . NoError ( t , err )
config := struct {
Keys [ ] * attestation . KeyMetadata ` json:"keys" `
} {
Keys : [ ] * attestation . KeyMetadata { keys } ,
}
keysYaml , err := yaml . Marshal ( config )
require . NoError ( t , err )
2024-06-06 09:59:32 +01:00
testCases := [ ] struct {
2024-08-28 09:53:52 +01:00
name string
signTL bool
policyDir string
imageName string
2024-08-23 09:33:30 +01:00
expectedNonSuccess Outcome
2024-10-15 16:07:26 +01:00
spitConfig bool
param string
2024-06-06 09:59:32 +01:00
} {
2024-07-12 17:09:41 +01:00
{ name : "happy path" , signTL : true , policyDir : PassNoTLPolicyDir } ,
{ name : "sign tl, verify no tl" , signTL : true , policyDir : PassPolicyDir } ,
{ name : "no tl" , signTL : false , policyDir : PassPolicyDir } ,
2024-09-05 14:08:55 +01:00
{ name : "mirror" , signTL : false , policyDir : PassMirrorPolicyDir , imageName : "mirror.org/library/test-image:test" } ,
{ name : "mirror no match" , signTL : false , policyDir : PassMirrorPolicyDir , imageName : "incorrect.org/library/test-image:test" , expectedNonSuccess : OutcomeNoPolicy } ,
2024-08-21 18:01:11 +01:00
{ name : "verify inputs" , signTL : false , policyDir : InputsPolicyDir } ,
2024-10-15 16:07:26 +01:00
{ name : "mirror with verification" , signTL : false , policyDir : LocalKeysPolicy , imageName : "mirror.org/library/test-image:test" , spitConfig : true } ,
{ name : "policy with input params" , spitConfig : true , signTL : false , policyDir : LocalParamPolicy , param : "bar" } ,
{ name : "policy without expected param" , spitConfig : true , signTL : false , policyDir : LocalParamPolicy , param : "baz" , expectedNonSuccess : OutcomeFailure } ,
2024-06-06 09:59:32 +01:00
}
2024-09-30 20:53:13 +01:00
attIdx , err := oci . IndexFromPath ( test . UnsignedTestIndex ( ) )
2024-06-06 09:59:32 +01:00
assert . NoError ( t , err )
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
2024-09-18 13:34:10 +01:00
opts := & attestation . SigningOptions { }
if tc . signTL {
opts . TransparencyLog = tlog . GetMockTL ( )
2024-06-06 09:59:32 +01:00
}
2024-10-15 16:07:26 +01:00
if tc . spitConfig {
// write keysYaml to config.yaml in LocalKeysPolicy.
err = os . WriteFile ( filepath . Join ( tc . policyDir , "config.yaml" ) , keysYaml , 0 o600 )
require . NoError ( t , err )
}
2024-06-06 09:59:32 +01:00
2024-07-05 09:29:14 +01:00
signedManifests , err := SignStatements ( ctx , attIdx . Index , signer , opts )
require . NoError ( t , err )
signedIndex := attIdx . Index
2024-07-16 10:05:17 +01:00
signedIndex , err = attestation . UpdateIndexImages ( signedIndex , signedManifests , attestation . WithReplacedLayers ( true ) )
2024-07-05 09:29:14 +01:00
require . NoError ( t , err )
2024-07-12 17:09:41 +01:00
imageName := tc . imageName
if imageName == "" {
imageName = attIdx . Name
}
2024-06-06 09:59:32 +01:00
// output signed attestations
2024-08-05 13:24:58 -05:00
spec , err := oci . ParseImageSpec ( oci . LocalPrefix + outputLayout , oci . WithPlatform ( LinuxAMD64 ) )
require . NoError ( t , err )
2024-09-09 14:22:17 +01:00
err = oci . SaveIndex ( ctx , [ ] * oci . ImageSpec { spec } , signedIndex , imageName )
2024-08-05 13:24:58 -05:00
require . NoError ( t , err )
2024-06-06 09:59:32 +01:00
2024-08-01 15:35:15 +01:00
policyOpts := & policy . Options {
2024-06-06 09:59:32 +01:00
LocalPolicyDir : tc . policyDir ,
2024-08-28 09:53:52 +01:00
DisableTUF : true ,
2024-09-05 14:08:55 +01:00
Debug : true ,
2024-06-06 09:59:32 +01:00
}
2024-10-15 16:07:26 +01:00
if tc . signTL {
getTL := func ( _ context . Context , _ * attestation . VerifyOptions ) ( tlog . TransparencyLog , error ) {
return tlog . GetMockTL ( ) , nil
}
verifier , err := attestation . NewVerfier ( attestation . WithLogVerifierFactory ( getTL ) )
require . NoError ( t , err )
policyOpts . AttestationVerifier = verifier
}
if tc . param != "" {
policyOpts . Parameters = policy . Parameters { "foo" : tc . param }
}
2024-08-05 13:24:58 -05:00
results , err := Verify ( ctx , spec , policyOpts )
2024-08-23 09:33:30 +01:00
require . NoError ( t , err )
if tc . expectedNonSuccess != "" {
assert . Equal ( t , tc . expectedNonSuccess , results . Outcome )
2024-07-12 17:09:41 +01:00
return
}
2024-09-05 14:08:55 +01:00
if results . Outcome == OutcomeFailure {
t . Logf ( "Violations: %v" , results . Violations )
}
2024-06-06 09:59:32 +01:00
assert . Equal ( t , OutcomeSuccess , results . Outcome )
2024-07-12 17:09:41 +01:00
platform , err := oci . ParsePlatform ( LinuxAMD64 )
require . NoError ( t , err )
2024-08-21 18:01:11 +01:00
ref , err := reference . ParseNormalizedNamed ( attIdx . Name )
require . NoError ( t , err )
expectedPURL , _ , err := oci . RefToPURL ( ref , platform )
2024-07-12 17:09:41 +01:00
require . NoError ( t , err )
2024-08-01 15:35:15 +01:00
assert . Equal ( t , expectedPURL , results . Input . PURL )
2024-06-06 09:59:32 +01:00
} )
}
}
2024-08-23 09:33:30 +01:00
func TestDefaultOptions ( t * testing . T ) {
testCases := [ ] struct {
name string
tufOpts * tuf . ClientOptions
localTargetsDir string
2024-09-18 21:11:55 +01:00
attestationStyle mapping . AttestationStyle
2024-08-23 09:33:30 +01:00
referrersRepo string
expectedError string
2024-08-28 09:53:52 +01:00
disableTuf bool
localPolicyDir string
2024-08-23 09:33:30 +01:00
} {
{ name : "empty" } ,
{ name : "tufClient provided" , tufOpts : & tuf . ClientOptions { MetadataSource : "a" , TargetsSource : "b" } } ,
{ name : "localTargetsDir provided" , localTargetsDir : test . CreateTempDir ( t , "" , TestTempDir ) } ,
2024-09-18 21:11:55 +01:00
{ name : "attestationStyle provided" , attestationStyle : mapping . AttestationStyleAttached } ,
2024-08-23 09:33:30 +01:00
{ name : "referrersRepo provided" , referrersRepo : "referrers" } ,
2024-09-18 21:11:55 +01:00
{ name : "referrersRepo provided with attached" , referrersRepo : "referrers" , attestationStyle : mapping . AttestationStyleAttached , expectedError : "referrers repo specified but attestation source not set to referrers" } ,
2024-08-28 09:53:52 +01:00
{ name : "tuf disabled and no local-policy-dir" , disableTuf : true , expectedError : "local policy dir must be set if not using TUF" } ,
{ name : "tuf disabled but options set" , disableTuf : true , tufOpts : & tuf . ClientOptions { MetadataSource : "a" , TargetsSource : "b" } , localPolicyDir : "foo" , expectedError : "TUF client options set but TUF disabled" } ,
2024-08-23 09:33:30 +01:00
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
defaultTargets , err := defaultLocalTargetsDir ( )
require . NoError ( t , err )
opts := & policy . Options {
TUFClientOptions : tc . tufOpts ,
LocalTargetsDir : tc . localTargetsDir ,
AttestationStyle : tc . attestationStyle ,
ReferrersRepo : tc . referrersRepo ,
2024-08-28 09:53:52 +01:00
DisableTUF : tc . disableTuf ,
LocalPolicyDir : tc . localPolicyDir ,
2024-08-23 09:33:30 +01:00
}
err = populateDefaultOptions ( opts )
if tc . expectedError != "" {
require . Error ( t , err )
assert . Equal ( t , tc . expectedError , err . Error ( ) )
return
}
require . NoError ( t , err )
if tc . localTargetsDir != "" {
assert . Equal ( t , tc . localTargetsDir , opts . LocalTargetsDir )
} else {
assert . Equal ( t , defaultTargets , opts . LocalTargetsDir )
}
if tc . attestationStyle != "" {
assert . Equal ( t , tc . attestationStyle , opts . AttestationStyle )
} else {
2024-09-18 21:11:55 +01:00
assert . Equal ( t , mapping . AttestationStyleReferrers , opts . AttestationStyle )
2024-08-23 09:33:30 +01:00
}
if tc . tufOpts != nil {
assert . Equal ( t , tc . tufOpts , opts . TUFClientOptions )
} else {
assert . NotNil ( t , opts . TUFClientOptions )
}
if tc . referrersRepo != "" {
assert . Equal ( t , tc . referrersRepo , opts . ReferrersRepo )
} else {
assert . Empty ( t , opts . ReferrersRepo )
}
} )
}
}
2024-09-18 13:34:10 +01:00
// LoadKeyMetadata loads the key metadata for the given signer verifier.
func GenKeyMetadata ( sv dsse . SignerVerifier ) ( * attestation . KeyMetadata , error ) {
pub := sv . Public ( )
pem , err := test . PublicKeyToPEM ( pub )
if err != nil {
return nil , fmt . Errorf ( "failed to convert public key to PEM: %w" , err )
}
id , err := sv . KeyID ( )
if err != nil {
return nil , err
}
return & attestation . KeyMetadata {
ID : id ,
Status : "active" ,
SigningFormat : "dssev1" ,
From : time . Now ( ) ,
PEM : pem ,
} , nil
}