diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c85c1eb..41a5673 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,25 +1,65 @@ name: Continuous Integration -on: - push: +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BIFROEST_VENDOR: "Engity GmbH" + +on: pull_request: types: - opened - reopened + - synchronize + - ready_for_review jobs: + evaluate: + name: Evaluate + runs-on: ubuntu-latest + outputs: + commit: "${{ steps.refs.outputs.commit }}" + version: "${{ steps.refs.outputs.version }}" + ref: "${{ steps.refs.outputs.ref }}" + pr: "${{ steps.refs.outputs.pr }}" + stage-binary: "${{ steps.refs.outputs.stage-binary }}" + stage-archive: "${{ steps.refs.outputs.stage-archive }}" + stage-image: "${{ steps.refs.outputs.stage-image }}" + stage-digest: "${{ steps.refs.outputs.stage-digest }}" + stage-publish: "${{ steps.refs.outputs.stage-publish }}" + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Go + uses: actions/setup-go@v5 + with: + cache: false + go-version-file: go.mod + check-latest: true + + - name: Cache Go + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Execute + id: refs + run: | + go run ./cmd/build evaluate-environment --log.colorMode=always + test: name: Tests + needs: [ evaluate ] strategy: matrix: os: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: stable - - name: Install Ubuntu dependencies if: ${{ matrix.os == 'ubuntu-latest' }} run: sudo apt install libpam0g-dev @@ -29,7 +69,14 @@ jobs: with: fetch-depth: 0 - - name: Cache + - name: Install Go + uses: actions/setup-go@v5 + with: + cache: false + go-version-file: go.mod + check-latest: true + + - name: Cache Go uses: actions/cache@v4 with: path: ~/go/pkg/mod @@ -40,6 +87,10 @@ jobs: - name: Install goveralls run: go install github.com/mattn/goveralls@latest + - name: Install dependencies + run: | + go mod download + - name: Test run: | mkdir -p var @@ -53,26 +104,17 @@ jobs: goveralls "-coverprofile=profile.cov" "-service=github" "-parallel" "-flagname=go-${{ matrix.os }}" package: name: Package - strategy: - matrix: - os: [ ubuntu-latest ] - runs-on: ${{ matrix.os }} + needs: [ evaluate ] + runs-on: "ubuntu-latest" + container: + image: ghcr.io/engity-com/build-images/go steps: - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: stable - - - name: Install Ubuntu dependencies - if: ${{ matrix.os == 'ubuntu-latest' }} - run: sudo apt install libpam0g-dev - - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Cache + - name: Cache Go uses: actions/cache@v4 with: path: ~/go/pkg/mod @@ -80,28 +122,37 @@ jobs: restore-keys: | ${{ runner.os }}-go- - - name: Install Syft - uses: anchore/sbom-action/download-syft@v0.17.2 - - - name: GoReleaser - uses: goreleaser/goreleaser-action@v6 + - name: Cache images dependencies + uses: actions/cache@v4 with: - distribution: goreleaser - version: "~> v2" - args: release --snapshot --clean - env: - BIFROEST_VENDOR: engity + path: .cache/dependencies/images + key: images-dependencies + restore-keys: images-dependencies + + - name: Git configure + run: | + git config --global --add safe.directory $(pwd) + + - name: Install dependencies + run: | + go mod download + + - name: Build + run: | + go run ./cmd/build build --log.colorMode=always - name: Archive package results + if: needs.evaluate.outputs.stage-publish == 'true' uses: actions/upload-artifact@v4 with: retention-days: 1 name: dist path: | - var/dist/*.tgz* - var/dist/*.zip* + var/dist/**/* + doc: name: Documentation + needs: [ evaluate ] runs-on: ubuntu-latest steps: - name: Checkout code @@ -115,6 +166,12 @@ jobs: with: python-version: 3.x + - name: Cache + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ hashFiles('docs/requirements.txt') }} + - name: Install dependencies run: | pip install -r docs/requirements.txt @@ -124,7 +181,16 @@ jobs: mkdocs --color build -c - name: Deploy + id: deploy + if: needs.evaluate.outputs.stage-publish == 'true' uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - command: pages deploy --project-name=bifroest-engity-org var/doc + command: pages deploy --branch=${{ needs.evaluate.outputs.version }} --commit-dirty=true --project-name=bifroest-engity-org var/doc + + - name: Report + if: needs.evaluate.outputs.stage-publish == 'true' + env: + DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }} + run: | + echo "Documentation is available at ${DEPLOYMENT_URL}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a686a54..d56c2a8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,11 +1,11 @@ name: Lint on: - push: - pull_request: types: - opened - reopened + - synchronize + - ready_for_review permissions: contents: read @@ -18,11 +18,17 @@ jobs: os: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 - - uses: actions/setup-go@v5 + - name: Install Go + uses: actions/setup-go@v5 with: - go-version: stable + cache: false + go-version-file: go.mod + check-latest: true - name: Install Ubuntu dependencies if: ${{ matrix.os == 'ubuntu-latest' }} diff --git a/.github/workflows/pr-update.yml b/.github/workflows/pr-update.yml new file mode 100644 index 0000000..274cc04 --- /dev/null +++ b/.github/workflows/pr-update.yml @@ -0,0 +1,52 @@ +name: Pull-Requests Updates + +env: + GITHUB_TOKEN: ${{ github.token }} + +concurrency: + cancel-in-progress: true + group: ${{ github.workflow }}-${{github.event.number}} + +on: + pull_request: + types: + - labeled + - unlabeled + - closed + +jobs: + build: + runs-on: ubuntu-latest + name: Inspect + if: github.event_name == 'pull_request' + permissions: + pull-requests: read + actions: write + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Go + uses: actions/setup-go@v5 + with: + cache: false + go-version-file: go.mod + check-latest: true + + - name: Cache Go + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Execute + id: refs + run: | + set -ex + go run ./cmd/build inspect-pr-action --log.colorMode=always "${{github.event.action}}" "${{github.event.label.name}}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32397a8..6985f80 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,9 @@ name: "Release" +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BIFROEST_VENDOR: "Engity GmbH" + on: release: types: [ published ] @@ -14,24 +18,15 @@ jobs: release: name: "Release" runs-on: ubuntu-latest + container: + image: ghcr.io/engity-com/build-images/go steps: - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: stable - - - name: Install Ubuntu dependencies - run: sudo apt install libpam0g-dev - - - name: Install Syft - uses: anchore/sbom-action/download-syft@v0.17.2 - - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Cache + - name: Cache Go uses: actions/cache@v4 with: path: ~/go/pkg/mod @@ -39,20 +34,25 @@ jobs: restore-keys: | ${{ runner.os }}-go- + - name: Cache images dependencies + uses: actions/cache@v4 + with: + path: .cache/dependencies/images + key: images-dependencies + restore-keys: images-dependencies + + - name: Install dependencies + run: | + go mod download + - name: Test run: | mkdir -p var go test -v ./... - - name: GoReleaser - uses: goreleaser/goreleaser-action@v6 - with: - distribution: goreleaser - version: "~> v2" - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BIFROEST_VENDOR: engity + - name: Build/Release + run: | + go run ./cmd/build build --log.colorMode=always documentation: name: "Documentation" @@ -73,6 +73,12 @@ jobs: run: | pip install -r docs/requirements.txt + - name: Cache + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ hashFiles('docs/requirements.txt') }} + - name: Setup Git run: | git config user.email "info@engity.com" diff --git a/.goreleaser.yaml b/.goreleaser.yaml deleted file mode 100644 index 993a925..0000000 --- a/.goreleaser.yaml +++ /dev/null @@ -1,112 +0,0 @@ -version: 2 - -project_name: "Engity's Bifröst" -dist: var/dist -report_sizes: true - -before: - hooks: - - go mod tidy - - go generate ./... - -builds: - - id: generic-tgz - main: ./cmd/bifroest - binary: bifroest - goos: - - linux - env: - - CGO_ENABLED=0 - goarch: - - amd64 - - arm64 - ldflags: - - "-s -w -X main.edition=generic -X main.version={{.Version}} -X main.revision={{.Commit}} -X main.buildAt={{.Date}} -X main.vendor={{envOrDefault `BIFROEST_VENDOR` `unknown`}}" - - - id: generic-zip - main: ./cmd/bifroest - binary: bifroest - goos: - - windows - env: - - CGO_ENABLED=0 - goarch: - - amd64 - - arm64 - ldflags: - - "-s -w -X main.edition=generic -X main.version={{.Version}} -X main.revision={{.Commit}} -X main.buildAt={{.Date}} -X main.vendor={{envOrDefault `BIFROEST_VENDOR` `unknown`}}" - - - id: extended-tgz - main: ./cmd/bifroest - binary: bifroest - goos: - - linux - env: - - CGO_ENABLED=1 - goarch: - - amd64 - ldflags: - - "-s -w -X main.edition=extended -X main.version={{.Version}} -X main.revision={{.Commit}} -X main.buildAt={{.Date}} -X main.vendor={{envOrDefault `BIFROEST_VENDOR` `unknown`}}" - -archives: - - id: generic-tgz - format: tgz - builds: - - generic-tgz - name_template: "bifroest-{{ .Os }}-{{ .Arch }}-generic" - files: - - LICENSE - - README.md - - SECURITY.md - - contrib/* - builds_info: - group: root - owner: root - - - id: generic-zip - format: zip - builds: - - generic-zip - name_template: "bifroest-{{ .Os }}-{{ .Arch }}-generic" - files: - - LICENSE - - README.md - - SECURITY.md - - contrib/* - builds_info: - group: root - owner: root - - - id: extended-tgz - format: tgz - builds: - - extended-tgz - name_template: "bifroest-{{ .Os }}-{{ .Arch }}-extended" - files: - - LICENSE - - README.md - - SECURITY.md - - contrib/* - builds_info: - group: root - owner: root - -##TODO! We should consider to activate this in the future. -##For now it produces too many files (including the checksums) to be very helpful. -#sboms: -# - id: generic -# artifacts: archive - -checksum: - split: true - -release: - github: - owner: engity-com - name: bifroest - draft: true - replace_existing_artifacts: true - -changelog: - use: github - sort: asc diff --git a/cmd/build/arch.go b/cmd/build/arch.go new file mode 100644 index 0000000..14615c3 --- /dev/null +++ b/cmd/build/arch.go @@ -0,0 +1,232 @@ +package main + +import ( + "fmt" + "runtime" + "strings" + + "github.com/engity-com/bifroest/pkg/common" +) + +type arch uint8 + +const ( + archUnknown arch = iota + arch386 + archAmd64 + archArmV6 + archArmV7 + archArm64 + archMips64Le + archRiscV64 + + fromDefaultLinux = "scratch" + fromDefaultLinuxExtended = "ubuntu" + fromDefaultWindows = "mcr.microsoft.com/windows/nanoserver:ltsc2022" +) + +var goarch = func() arch { + var buf arch + common.Must(buf.Set(runtime.GOARCH)) + return buf +}() + +func (this arch) String() string { + v, ok := archToDetails[this] + if !ok { + return fmt.Sprintf("illegal-arch-%d", this) + } + return v.name +} + +func (this arch) ociString() string { + v, ok := archToDetails[this] + if !ok { + return fmt.Sprintf("illegal-arch-%d", this) + } + if v.oci != "" { + return v.oci + } + return v.name +} + +func (this *arch) Set(plain string) error { + v, ok := stringToArch[plain] + if !ok { + return fmt.Errorf("illegal-arch: %s", plain) + } + *this = v + return nil +} + +func (this arch) isOsSupported(o os) bool { + return this.details().isOsSupported(o) +} + +func (this arch) setToEnv(o os, assumedGoos os, assumedGoarch arch, env interface{ setEnv(key, val string) }) { + this.details().setToEnv(o, this, assumedGoos, assumedGoarch, env) +} + +func (this arch) details() archDetails { + return archToDetails[this] +} + +type archs []arch + +func (this archs) String() string { + return strings.Join(this.Strings(), ",") +} + +func (this archs) Strings() []string { + strs := make([]string, len(this)) + for i, v := range this { + strs[i] = v.String() + } + return strs +} + +func (this *archs) Set(plain string) error { + parts := strings.Split(plain, ",") + buf := make(archs, len(parts)) + for i, part := range parts { + part = strings.TrimSpace(part) + if err := buf[i].Set(part); err != nil { + return err + } + } + *this = buf + return nil +} + +type archDetails struct { + name string + bare string + oci string + + i386 string + arm string + amd64 string + + os map[os]archOsDetails +} + +func (this archDetails) isOsSupported(o os) bool { + _, ok := this.os[o] + return ok +} + +func (this archDetails) setToEnv(o os, a arch, assumedGoos os, assumedGoarch arch, env interface{ setEnv(key, val string) }) { + osDetails, ok := this.os[o] + if !ok { + return + } + + o.setToEnv(env) + if v := this.bare; v != "" { + env.setEnv("GOARCH", v) + } else { + env.setEnv("GOARCH", this.name) + } + if v := this.i386; v != "" { + env.setEnv("GO386", v) + } + if v := this.arm; v != "" { + env.setEnv("GOARM", v) + } + if v := this.amd64; v != "" { + env.setEnv("GOAMD64", v) + } + + osDetails.setToEnv(o, a, assumedGoos, assumedGoarch, env) +} + +type archOsDetails struct { + fromImage string + fromImageExtended string + build map[archBuildKey]archBuildDetails +} + +func (this archOsDetails) setToEnv(o os, a arch, assumedGoos os, assumedGoarch arch, env interface{ setEnv(key, val string) }) { + this.build[archBuildKey{assumedGoos, assumedGoarch}].setToEnv(o, a, assumedGoos, assumedGoarch, env) +} + +type archBuildKey struct { + os os + arch arch +} + +type archBuildDetails struct { + crossCc string +} + +func (this archBuildDetails) setToEnv(o os, a arch, assumedGoos os, assumedGoarch arch, env interface{ setEnv(key, val string) }) { + if this.crossCc != "" && (assumedGoos != o || assumedGoarch != a) { + env.setEnv("CC", this.crossCc) + } +} + +var ( + // See https://go.dev/doc/install/source for more details + archToDetails = map[arch]archDetails{ + arch386: {name: "386", i386: "sse2", os: map[os]archOsDetails{ + osLinux: {fromImage: fromDefaultLinux, build: map[archBuildKey]archBuildDetails{ + {osLinux, archAmd64}: {"i686-linux-gnu-gcc"}, + {osWindows, archAmd64}: {}, + }}, + }}, + archAmd64: {name: "amd64", amd64: "v1", os: map[os]archOsDetails{ + osLinux: {fromImage: fromDefaultLinux, fromImageExtended: fromDefaultLinuxExtended, build: map[archBuildKey]archBuildDetails{ + {osLinux, archAmd64}: {"x86-64-linux-gnu-gcc"}, + {osWindows, archAmd64}: {}, + }}, + osWindows: {fromImage: fromDefaultWindows}, + }}, + archArmV6: {name: "armv6", bare: "arm", oci: "arm/v6", arm: "6", os: map[os]archOsDetails{ + osLinux: {fromImage: fromDefaultLinux, build: map[archBuildKey]archBuildDetails{ + {osLinux, archAmd64}: {"arm-linux-gnueabihf-gcc"}, + {osWindows, archAmd64}: {}, + }}, + }}, + archArmV7: {name: "armv7", bare: "arm", oci: "arm/v7", arm: "7", os: map[os]archOsDetails{ + osLinux: {fromImage: fromDefaultLinux, fromImageExtended: fromDefaultLinuxExtended, build: map[archBuildKey]archBuildDetails{ + {osLinux, archAmd64}: {"arm-linux-gnueabihf-gcc"}, + {osWindows, archAmd64}: {}, + }}, + }}, + archArm64: {name: "arm64", os: map[os]archOsDetails{ + osLinux: {fromImage: fromDefaultLinux, fromImageExtended: fromDefaultLinuxExtended, build: map[archBuildKey]archBuildDetails{ + {osLinux, archAmd64}: {"aarch64-linux-gnu-gcc"}, + {osWindows, archAmd64}: {}, + }}, + osWindows: {}, + }}, + archMips64Le: {name: "mips64le", os: map[os]archOsDetails{ + osLinux: {fromImage: fromDefaultLinux, build: map[archBuildKey]archBuildDetails{ + {osLinux, archAmd64}: {"mips64el-linux-gnuabi64-gcc"}, + {osWindows, archAmd64}: {}, + }}, + }}, + archRiscV64: {name: "riscv64", os: map[os]archOsDetails{ + osLinux: {fromImage: fromDefaultLinux, build: map[archBuildKey]archBuildDetails{ + {osLinux, archAmd64}: {}, + {osWindows, archAmd64}: {}, + }}, + }}, + } + stringToArch = func(in map[arch]archDetails) map[string]arch { + result := make(map[string]arch, len(in)) + for k, v := range in { + result[v.name] = k + } + return result + }(archToDetails) + allArchVariants = func(in map[arch]archDetails) archs { + result := make(archs, len(in)) + var i int + for k := range in { + result[i] = k + i++ + } + return result + }(archToDetails) +) diff --git a/cmd/build/archive-format.go b/cmd/build/archive-format.go new file mode 100644 index 0000000..32f696d --- /dev/null +++ b/cmd/build/archive-format.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" +) + +type archiveFormat uint8 + +const ( + packFormatTgz archiveFormat = iota + packFormatZip +) + +func (this archiveFormat) String() string { + v, ok := packFormatToString[this] + if !ok { + return fmt.Sprintf("illegal-archive-format-%d", this) + } + return v +} + +func (this archiveFormat) ext() string { + return "." + this.String() +} + +func (this *archiveFormat) Set(plain string) error { + v, ok := stringToPackFormat[plain] + if !ok { + return fmt.Errorf("illegal-archive-format: %s", plain) + } + *this = v + return nil +} + +var ( + packFormatToString = map[archiveFormat]string{ + packFormatTgz: "tgz", + packFormatZip: "zip", + } + stringToPackFormat = func(in map[archiveFormat]string) map[string]archiveFormat { + result := make(map[string]archiveFormat, len(in)) + for k, v := range in { + result[v] = k + } + return result + }(packFormatToString) +) diff --git a/cmd/build/base.go b/cmd/build/base.go new file mode 100644 index 0000000..7bcbeff --- /dev/null +++ b/cmd/build/base.go @@ -0,0 +1,253 @@ +package main + +import ( + "context" + "fmt" + "math" + "os/user" + "runtime" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/alecthomas/kingpin" + "github.com/echocat/slf4g" +) + +var ( + currentUserName = func() string { + current, err := user.Current() + if err != nil { + return "johndoe" + } + return current.Username + }() +) + +func newBase() *base { + result := &base{ + waitTimeout: time.Second * 3, + actor: currentUserName, + title: "Engity's Bifröst", + } + result.repo = newRepo(result) + result.build = newBuild(result) + result.exec = newExec(result) + result.dependencies = newDependencies(result) + return result +} + +type base struct { + repo *repo + build *build + *exec + dependencies *dependencies + + waitTimeout time.Duration + actor string + title string + rawCommit string + rawRef string + rawHeadRef string + rawPr uint + + optionsOutputFilename string + summaryOutputFilename string + + versionP atomic.Pointer[version] + commitP atomic.Pointer[string] + refP atomic.Pointer[string] + prP atomic.Pointer[uint] +} + +func (this *base) init(ctx context.Context, app *kingpin.Application) { + app.Flag("waitTimeout", ""). + PlaceHolder(""). + DurationVar(&this.waitTimeout) + app.Flag("actor", ""). + Default(this.actor). + Envar("GITHUB_ACTOR"). + PlaceHolder(""). + StringVar(&this.actor) + app.Flag("title", ""). + Default(this.title). + PlaceHolder(""). + StringVar(&this.title) + app.Flag("commit", ""). + Envar("GITHUB_SHA"). + PlaceHolder("<sha>"). + StringVar(&this.rawCommit) + app.Flag("ref", ""). + Envar("GITHUB_REF_NAME"). + PlaceHolder("<ref>"). + StringVar(&this.rawRef) + app.Flag("headRef", ""). + Envar("GITHUB_HEAD_REF"). + PlaceHolder("<ref>"). + StringVar(&this.rawHeadRef) + app.Flag("pr", ""). + PlaceHolder("<prNumber>"). + UintVar(&this.rawPr) + app.Flag("optionsOutputFilename", ""). + Envar("GITHUB_OUTPUT"). + PlaceHolder("<filename>"). + StringVar(&this.optionsOutputFilename) + app.Flag("summaryOutputFilename", ""). + Envar("GITHUB_STEP_SUMMARY"). + PlaceHolder("<filename>"). + StringVar(&this.summaryOutputFilename) + + app.Command("status", ""). + Action(func(*kingpin.ParseContext) error { + return this.status(ctx) + }) + + this.repo.init(ctx, app) + this.build.init(ctx, app) + this.exec.init(ctx, app) + this.dependencies.init(ctx, app) +} + +func (this *base) status(ctx context.Context) error { + commit, err := this.commit(ctx) + if err != nil { + return err + } + ref, err := this.ref(ctx) + if err != nil { + return err + } + v, err := this.version(ctx) + if err != nil { + return err + } + + log.With("commit", commit). + With("version", v). + With("ref", ref). + Info() + + return nil +} + +func (this *base) version(ctx context.Context) (version, error) { + for { + if v := this.versionP.Load(); v != nil { + return *v, nil + } + + ref, err := this.ref(ctx) + if err != nil { + return version{}, err + } + + var v version + if strings.HasPrefix(ref, "v") && v.Set(ref) == nil { + if err := v.evaluateLatest(this.repo.releases.allSemver(ctx)); err != nil { + return version{}, err + } + } else if pr := this.pr(); pr > 0 { + v.raw = fmt.Sprintf("pr-%d", pr) + } else { + v.raw = versionNormalizePattern.ReplaceAllString(ref, "-") + "-development" + } + + if this.versionP.CompareAndSwap(nil, &v) { + return v, nil + } + runtime.Gosched() + } +} + +func (this *base) ref(ctx context.Context) (string, error) { + for { + v := this.refP.Load() + if v != nil { + return *v, nil + } + nv, err := this.resolveRef(ctx) + if err != nil { + return "", err + } + if this.refP.CompareAndSwap(nil, &nv) { + return nv, nil + } + runtime.Gosched() + } +} + +func (this *base) resolveRef(ctx context.Context) (string, error) { + if v := this.rawHeadRef; v != "" { + return v, nil + } + if v := this.rawRef; v != "" { + return v, nil + } + v, err := this.exec.execute(ctx, "git", "rev-parse", "--abbrev-ref", "HEAD"). + doAndGet() + if err != nil { + return "", fmt.Errorf("cannot retrieve current ref: %w", err) + } + return v, nil +} + +func (this *base) pr() uint { + for { + v := this.prP.Load() + if v != nil { + return *v + } + nv := this.resolvePr() + if this.prP.CompareAndSwap(nil, &nv) { + return nv + } + runtime.Gosched() + } +} + +func (this *base) resolvePr() uint { + if v := this.rawPr; v != 0 { + return v + } + if v := this.rawRef; v != "" { + if !strings.HasSuffix(v, "/merge") { + return 0 + } + n, _ := strconv.ParseUint(strings.TrimSuffix(v, "/merge"), 10, 64) + if n > uint64(math.MaxUint) { + return 0 // or handle the error appropriately + } + return uint(n) + } + return 0 +} + +func (this *base) commit(ctx context.Context) (string, error) { + for { + v := this.commitP.Load() + if v != nil { + return *v, nil + } + nv, err := this.resolveCommit(ctx) + if err != nil { + return "", err + } + if this.commitP.CompareAndSwap(nil, &nv) { + return nv, nil + } + runtime.Gosched() + } +} + +func (this *base) resolveCommit(ctx context.Context) (string, error) { + if v := this.rawCommit; v != "" { + return v, nil + } + v, err := this.execute(ctx, "git", "rev-parse", "--verify", "HEAD"). + doAndGet() + if err != nil { + return "", fmt.Errorf("cannot retrieve current commit: %w", err) + } + return v, nil +} diff --git a/cmd/build/build-archive-tgz.go b/cmd/build/build-archive-tgz.go new file mode 100644 index 0000000..9466663 --- /dev/null +++ b/cmd/build/build-archive-tgz.go @@ -0,0 +1,79 @@ +package main + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + gos "os" + "time" + + "github.com/engity-com/bifroest/pkg/common" +) + +func (this *buildArchive) newTgzWriter(t time.Time, target io.Writer) (*buildArchiveTgzWriter, error) { + fail := func(err error) (*buildArchiveTgzWriter, error) { + return nil, err + } + + gzw, err := gzip.NewWriterLevel(target, gzip.BestCompression) + if err != nil { + return fail(err) + } + + return &buildArchiveTgzWriter{ + t, + gzw, + tar.NewWriter(gzw), + }, nil +} + +type buildArchiveTgzWriter struct { + time time.Time + gzip *gzip.Writer + tar *tar.Writer +} + +func (this *buildArchiveTgzWriter) Close() (rErr error) { + defer common.KeepCloseError(&rErr, this.gzip) + defer common.KeepCloseError(&rErr, this.tar) + return nil +} + +func (this *buildArchiveTgzWriter) addFile(name, sourceFn string, mode gos.FileMode) (rErr error) { + fail := func(err error) error { + return fmt.Errorf("cannot write file %q (source: %q): %w", name, sourceFn, err) + } + + sf, err := gos.Open(sourceFn) + if err != nil { + return fail(err) + } + defer common.KeepCloseError(&rErr, sf) + sfs, err := sf.Stat() + if err != nil { + return fail(err) + } + + if mode == 0 { + mode = sfs.Mode() + } + + if err := this.tar.WriteHeader(&tar.Header{ + Typeflag: tar.TypeReg, + Name: name, + Mode: int64(mode), + ModTime: this.time, + AccessTime: this.time, + ChangeTime: this.time, + Size: sfs.Size(), + }); err != nil { + return fail(err) + } + + if _, err := io.Copy(this.tar, sf); err != nil { + return fail(err) + } + + return nil +} diff --git a/cmd/build/build-archive-zip.go b/cmd/build/build-archive-zip.go new file mode 100644 index 0000000..9302561 --- /dev/null +++ b/cmd/build/build-archive-zip.go @@ -0,0 +1,70 @@ +package main + +import ( + "archive/zip" + "compress/flate" + "fmt" + "io" + gos "os" + "time" + + "github.com/engity-com/bifroest/pkg/common" +) + +func (this *buildArchive) newZipWriter(t time.Time, target io.Writer) (*buildArchiveZipWriter, error) { + zw := zip.NewWriter(target) + zw.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) { + return flate.NewWriter(out, flate.BestCompression) + }) + return &buildArchiveZipWriter{ + t, + zw, + }, nil +} + +type buildArchiveZipWriter struct { + time time.Time + zip *zip.Writer +} + +func (this *buildArchiveZipWriter) Close() (rErr error) { + defer common.KeepCloseError(&rErr, this.zip) + return nil +} + +func (this *buildArchiveZipWriter) addFile(name, sourceFn string, mode gos.FileMode) (rErr error) { + fail := func(err error) error { + return fmt.Errorf("cannot write file %q (source: %q): %w", name, sourceFn, err) + } + + sf, err := gos.Open(sourceFn) + if err != nil { + return fail(err) + } + defer common.KeepCloseError(&rErr, sf) + sfs, err := sf.Stat() + if err != nil { + return fail(err) + } + + if mode == 0 { + mode = sfs.Mode() + } + + header := zip.FileHeader{ + Name: name, + Modified: this.time, + UncompressedSize64: uint64(sfs.Size()), + } + header.SetMode(mode) + w, err := this.zip.CreateHeader(&header) + if err != nil { + return fail(err) + } + + if _, err := io.Copy(w, sf); err != nil { + return fail(err) + } + + return nil +} diff --git a/cmd/build/build-archive.go b/cmd/build/build-archive.go new file mode 100644 index 0000000..1fd34d1 --- /dev/null +++ b/cmd/build/build-archive.go @@ -0,0 +1,134 @@ +package main + +import ( + "context" + "fmt" + "io" + gos "os" + "time" + + "github.com/alecthomas/kingpin" + "github.com/echocat/slf4g" + "github.com/mattn/go-zglob" + + "github.com/engity-com/bifroest/pkg/common" +) + +func newBuildArchive(b *build) *buildArchive { + return &buildArchive{ + build: b, + + includedResources: []string{ + "README.md", + "LICENSE", + "SECURITY.md", + "contrib/**/*", + }, + } +} + +type buildArchive struct { + *build + + includedResources []string +} + +func (this *buildArchive) attach(cmd *kingpin.CmdClause) { + cmd.Flag("includedResource", ""). + PlaceHolder("<file>[,...]"). + StringsVar(&this.includedResources) +} + +func (this *buildArchive) create(ctx context.Context, binary *buildArtifact) (_ *buildArtifact, rErr error) { + format := binary.platform.os.archiveFormat() + fn := binary.platform.filenamePrefix(this.prefix) + format.ext() + + success := false + a, err := this.build.newBuildFileArtifact(ctx, binary.platform, buildArtifactTypeArchive, fn) + if err != nil { + return nil, err + } + defer common.IgnoreCloseErrorIfFalse(&success, a) + + fail := func(err error) (*buildArtifact, error) { + return nil, fmt.Errorf("cannot create %v: %w", a, err) + } + + l := log.With("platform", a.platform). + With("stage", buildStageArchive) + + start := time.Now() + l.Debug("building archive...") + + f, err := gos.OpenFile(a.filepath, gos.O_TRUNC|gos.O_CREATE|gos.O_WRONLY, 0644) + if err != nil { + return fail(err) + } + defer common.KeepCloseError(&rErr, f) + + baw, err := this.newWriter(binary, f) + if err != nil { + return fail(err) + } + defer common.KeepCloseError(&rErr, baw) + + if err := baw.addFile(this.prefix+binary.platform.os.execExt(), binary.filepath, 0755); err != nil { + return fail(err) + } + for _, res := range this.includedResources { + if err := this.addResource(res, baw); err != nil { + return fail(err) + } + } + + ld := l.With("duration", time.Since(start).Truncate(time.Millisecond)) + if l.IsDebugEnabled() { + ld.Debug("building archive... DONE!") + } else { + ld.Info("archive built") + } + + success = true + return a, nil +} + +func (this *buildArchive) addResource(src string, to buildArchiveWriter) error { + fail := func(err error) error { + return fmt.Errorf("cannot add resource %q: %w", src, err) + } + candidates, err := zglob.Glob(src) + if err != nil { + return fail(err) + } + + for _, candidate := range candidates { + fi, err := gos.Stat(candidate) + if err != nil { + return fail(err) + } + if !fi.IsDir() { + if err := to.addFile(candidate, candidate, fi.Mode()); err != nil { + return fail(err) + } + } + } + + return nil +} + +func (this *buildArchive) newWriter(binary *buildArtifact, w io.Writer) (buildArchiveWriter, error) { + format := binary.platform.os.archiveFormat() + switch binary.platform.os.archiveFormat() { + case packFormatTgz: + return this.newTgzWriter(binary.time, w) + case packFormatZip: + return this.newZipWriter(binary.time, w) + default: + return nil, fmt.Errorf("unknown archive format: %v", format) + } +} + +type buildArchiveWriter interface { + io.Closer + addFile(name, sourceFn string, mode gos.FileMode) error +} diff --git a/cmd/build/build-artifact.go b/cmd/build/build-artifact.go new file mode 100644 index 0000000..abcfcfc --- /dev/null +++ b/cmd/build/build-artifact.go @@ -0,0 +1,201 @@ +package main + +import ( + "fmt" + "iter" + gos "os" + "path" + "path/filepath" + "strings" + "sync" + + v1 "github.com/google/go-containerregistry/pkg/v1" + + "github.com/engity-com/bifroest/pkg/common" +) + +type buildArtifactCloser func() error + +type buildArtifact struct { + *platform + *buildContext + + t buildArtifactType + filepath string + ociImage v1.Image + ociIndex v1.ImageIndex + + onClose []buildArtifactCloser + lock sync.Mutex +} + +func (this *buildArtifact) toLdFlags(o os) string { + return this.platform.toLdFlags(o) + + " " + this.buildContext.toLdFlags(this.testing) +} + +func (this *buildArtifact) String() string { + return this.platform.String() + "/" + this.t.String() + ":" + this.name() +} + +func (this *buildArtifact) mediaType() string { + switch this.t { + case buildArtifactTypeDigest: + return "text/plain; charset=utf-8" + case buildArtifactTypeArchive: + switch strings.ToLower(path.Ext(this.name())) { + case ".tgz": + return "application/tar+gzip" + case ".zip": + return "application/zip" + default: + return "application/octet-stream" + } + default: + return "application/octet-stream" + } +} + +func (this *buildArtifact) name() string { + return filepath.Base(this.filepath) +} + +func (this *buildArtifact) Close() (rErr error) { + this.lock.Lock() + defer this.lock.Unlock() + + for _, closer := range this.onClose { + defer common.KeepError(&rErr, closer) + } + + return nil +} + +func (this *buildArtifact) addCloser(v buildArtifactCloser) { + this.lock.Lock() + defer this.lock.Unlock() + + this.onClose = append(this.onClose, v) +} + +type buildArtifactType uint8 + +const ( + buildArtifactTypeBinary buildArtifactType = iota + buildArtifactTypeArchive + buildArtifactTypeImagePlatform + buildArtifactTypeImage + buildArtifactTypeDigest +) + +func (this buildArtifactType) String() string { + v, ok := buildArtifactTypeToString[this] + if !ok { + return fmt.Sprintf("illegal-build-artifact-type-%d", this) + } + return v +} + +func (this buildArtifactType) canBePublished() bool { + switch this { + case buildArtifactTypeArchive, buildArtifactTypeDigest: + return true + default: + return false + } +} + +var ( + buildArtifactTypeToString = map[buildArtifactType]string{ + buildArtifactTypeBinary: "binary", + buildArtifactTypeArchive: "archive", + buildArtifactTypeImagePlatform: "imagePlatform", + buildArtifactTypeImage: "image", + buildArtifactTypeDigest: "digest", + } +) + +type buildArtifacts []*buildArtifact + +func (this buildArtifacts) Close() (rErr error) { + for _, v := range this { + defer common.KeepCloseError(&rErr, v) + } + return nil +} + +func (this buildArtifacts) onlyOfType(t buildArtifactType) iter.Seq[*buildArtifact] { + return this.filter(func(candidate *buildArtifact) bool { + return candidate.t == t + }) +} + +func (this buildArtifacts) withoutType(t buildArtifactType) iter.Seq[*buildArtifact] { + return this.filter(func(candidate *buildArtifact) bool { + return candidate.t != t + }) +} + +func (this buildArtifacts) filter(predicate func(*buildArtifact) bool) iter.Seq[*buildArtifact] { + return func(yield func(*buildArtifact) bool) { + for _, candidate := range this { + if predicate(candidate) && !yield(candidate) { + return + } + } + } +} + +func (this *buildArtifact) openFile() (*gos.File, error) { + if this.filepath == "" { + return nil, fmt.Errorf("cannot open file of non-file artifact: %v", this) + } + + return gos.Open(this.filepath) +} + +func (this *buildArtifact) createFile() (*gos.File, error) { + if this.filepath == "" { + return nil, fmt.Errorf("cannot create file of non-file artifact: %v", this) + } + + return gos.OpenFile(this.filepath, gos.O_CREATE|gos.O_TRUNC|gos.O_WRONLY, 0644) +} + +func (this *buildArtifact) toLayer(otherItems iter.Seq2[imageArtifactLayerItem, error]) (v1.Layer, error) { + if this.t != buildArtifactTypeBinary { + return nil, fmt.Errorf("cannot create layer of non-binary artifact: %v", this) + } + + items := common.JoinSeq2[imageArtifactLayerItem, error]( + common.SingleSeq2Of[imageArtifactLayerItem, error](imageArtifactLayerItem{ + sourceFile: this.filepath, + targetFile: this.platform.os.bifroestBinaryFilePath(), + mode: 755, + }, nil), + otherItems, + ) + + success := false + result, err := createImageArtifactLayer( + this.os, + strings.ReplaceAll(this.platform.String()+"-"+this.t.String(), "/", "-"), + this.time, + items, + ) + if err != nil { + return nil, err + } + defer common.IgnoreCloseErrorIfFalse(&success, result) + + this.addCloser(result.Close) + + success = true + return result.layer, nil +} + +// userOwnerAndGroupSID is a magic value needed to make the binary executable +// in a Windows container. +// +// owner: BUILTIN/Users group: BUILTIN/Users ($sddlValue="O:BUG:BU") +const windowsUserOwnerAndGroupSID = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA==" diff --git a/cmd/build/build-binary.go b/cmd/build/build-binary.go new file mode 100644 index 0000000..72185d9 --- /dev/null +++ b/cmd/build/build-binary.go @@ -0,0 +1,143 @@ +package main + +import ( + "context" + "fmt" + gos "os" + "strconv" + "strings" + "time" + + "github.com/alecthomas/kingpin" + "github.com/echocat/slf4g" + + "github.com/engity-com/bifroest/pkg/common" +) + +func newBuildBinary(b *build) *buildBinary { + return &buildBinary{ + build: b, + } +} + +type buildBinary struct { + *build +} + +func (this *buildBinary) attach(_ *kingpin.CmdClause) {} + +func (this *buildBinary) compile(ctx context.Context, p *platform) (*buildArtifact, error) { + fail := func(err error) (*buildArtifact, error) { + return nil, fmt.Errorf("cannot build %v: %w", *p, err) + } + + if err := p.assertBinarySupported(this.assumedBuildOs(), this.assumedBuildArch()); err != nil { + return fail(err) + } + + fn := p.filenamePrefix(this.prefix) + p.os.execExt() + + success := false + a, err := this.newBuildFileArtifact(ctx, p, buildArtifactTypeBinary, fn) + if err != nil { + return fail(err) + } + defer common.IgnoreCloseErrorIfFalse(&success, a) + + l := log.With("platform", p). + With("stage", buildStageBinary). + With("file", a.filepath) + + ldFlags := " -s -w " + a.toLdFlags(a.os) + + start := time.Now() + l.Debug("building binary...") + + var buildEnvPath string + createExec := func(cmd string, args ...string) (*execCmd, error) { + if this.wslBuildDistribution == "" { + return this.build.execute(ctx, cmd, args...), nil + } + wd, err := gos.Getwd() + if err != nil { + return nil, err + } + wd, err = this.translateToWslPath(wd) + if err != nil { + return nil, err + } + + f, err := gos.CreateTemp("", "bifroest-go-build-*.env") + if err != nil { + return nil, err + } + _ = f.Close() + + buildEnvPath = f.Name() + wslBuildEnvPath, err := this.translateToWslPath(buildEnvPath) + if err != nil { + return nil, err + } + + qargs := make([]string, len(args)+1) + qargs[0] = strconv.Quote(cmd) + for i, arg := range args { + qargs[i+1] = strconv.Quote(arg) + } + + result := this.build.execute(ctx, "wsl", + "-d", this.wslBuildDistribution, + "--cd", wd, + "bash", + "-c", "source "+strconv.Quote(wslBuildEnvPath)+"; "+strings.Join(qargs, " "), + ) + result.env = map[string]string{} + return result, nil + } + + outputFilePath := a.filepath + if this.wslBuildDistribution != "" { + outputFilePath, err = this.translateToWslPath(outputFilePath) + if err != nil { + return fail(err) + } + } + + ec, err := createExec("go", "build", "-ldflags", ldFlags, "-o", outputFilePath, "./cmd/bifroest") + if err != nil { + return fail(err) + } + ec.attachStd() + a.setToEnv(this.assumedBuildOs(), this.assumedBuildArch(), ec) + + if this.wslBuildDistribution != "" { + f, err := gos.OpenFile(buildEnvPath, gos.O_WRONLY|gos.O_TRUNC, 0) + if err != nil { + return fail(err) + } + defer func() { _ = gos.Remove(f.Name()) }() + defer common.IgnoreCloseError(f) + + for k, v := range ec.env { + if _, err := fmt.Fprintf(f, "export %s=%q\n", k, v); err != nil { + return fail(err) + } + } + if err := f.Close(); err != nil { + return fail(err) + } + } + if err := ec.do(); err != nil { + return fail(err) + } + + ld := l.With("duration", time.Since(start).Truncate(time.Millisecond)) + if l.IsDebugEnabled() { + ld.Debug("building binary... DONE!") + } else { + ld.Info("binary built") + } + + success = true + return a, nil +} diff --git a/cmd/build/build-digest.go b/cmd/build/build-digest.go new file mode 100644 index 0000000..f5d5b55 --- /dev/null +++ b/cmd/build/build-digest.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "crypto/sha256" + "fmt" + "io" + "path/filepath" + "time" + + "github.com/alecthomas/kingpin" + "github.com/echocat/slf4g" + + "github.com/engity-com/bifroest/pkg/common" +) + +func newBuildDigest(b *build) *buildDigest { + return &buildDigest{ + build: b, + } +} + +type buildDigest struct { + *build +} + +func (this *buildDigest) attach(_ *kingpin.CmdClause) {} + +func (this *buildDigest) create(_ context.Context, as buildArtifacts) (_ buildArtifacts, rErr error) { + if len(as) == 0 { + return as, nil + } + + success := false + result := &buildArtifact{ + platform: &platform{ + testing: as[0].testing, + }, + buildContext: as[0].buildContext, + t: buildArtifactTypeDigest, + filepath: as[0].buildContext.filepath("bifroest-checksums.txt"), + } + defer common.IgnoreCloseErrorIfFalse(&success, result) + + fail := func(err error) (buildArtifacts, error) { + return nil, fmt.Errorf("cannot create digest %v: %w", result, err) + } + + l := log.With("stage", buildStageDigest) + + start := time.Now() + l.Debug("building digest...") + + f, err := result.createFile() + if err != nil { + return fail(err) + } + defer common.KeepCloseError(&rErr, f) + + for _, a := range as { + if a.t.canBePublished() && a.filepath != "" { + sf, err := a.openFile() + if err != nil { + return fail(err) + } + + hash := sha256.New() + if _, err := io.Copy(hash, sf); err != nil { + return fail(err) + } + + if _, err := fmt.Fprintf(f, "%x %s\n", hash.Sum(nil), filepath.Base(a.filepath)); err != nil { + return fail(err) + } + } + } + + ld := l.With("duration", time.Since(start).Truncate(time.Millisecond)) + if l.IsDebugEnabled() { + ld.Debug("building digest... DONE!") + } else { + ld.Info("digest built") + } + + success = true + return append(as, result), nil +} diff --git a/cmd/build/build-image-layer.go b/cmd/build/build-image-layer.go new file mode 100644 index 0000000..7608ac7 --- /dev/null +++ b/cmd/build/build-image-layer.go @@ -0,0 +1,204 @@ +package main + +import ( + "archive/tar" + "fmt" + "io" + "iter" + gos "os" + "path" + "path/filepath" + "slices" + "strings" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/tarball" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/sys" +) + +type imageArtifactLayerItem struct { + sourceFile string + targetFile string + mode gos.FileMode +} + +func createImageArtifactLayer(os os, id string, time time.Time, items iter.Seq2[imageArtifactLayerItem, error]) (*buildImageLayer, error) { + fail := func(err error) (*buildImageLayer, error) { + return nil, fmt.Errorf("cannot create tar layer: %w", err) + } + + dir := filepath.Join("var", "layers") + _ = gos.MkdirAll(dir, 0755) + + fAlreadyClosed := false + success := false + f, err := gos.CreateTemp(dir, id+"-*.tar") + if err != nil { + return fail(err) + } + defer common.IgnoreErrorIfFalse(&success, func() error { return gos.Remove(f.Name()) }) + defer common.IgnoreCloseErrorIfFalse(&fAlreadyClosed, f) + + if err := createImageArtifactLayerTar(os, time, items, f); err != nil { + return fail(err) + } + common.IgnoreCloseError(f) + fAlreadyClosed = true + + result := &buildImageLayer{ + bufferFilename: f.Name(), + } + + if result.layer, err = tarball.LayerFromOpener(result.open); err != nil { + return fail(err) + } + + success = true + return result, nil +} + +func createImageArtifactLayerTar(os os, time time.Time, items iter.Seq2[imageArtifactLayerItem, error], target io.Writer) error { + fail := func(err error) error { + return err + } + failf := func(msg string, args ...any) error { + return fail(fmt.Errorf(msg, args...)) + } + + tw := tar.NewWriter(target) + + var format tar.Format + var paxRecords map[string]string + + writeHeader := func( + dir bool, + name string, + size int64, + mode int64, + ) error { + header := tar.Header{ + Name: name, + Size: size, + Mode: mode, + Format: format, + PAXRecords: paxRecords, + ModTime: time, + } + if dir { + header.Typeflag = tar.TypeDir + } else { + header.Typeflag = tar.TypeReg + } + return tw.WriteHeader(&header) + } + + adjustTargetFilename := func(v string) string { + // Paths needs to be always relative + if len(v) > 1 && (v[0] == '/' || v[0] == '\\') { + v = v[1:] + } + return v + } + var dirMode int64 = 0755 + alreadyCreatedDirectories := map[string]struct{}{} + + if os == osWindows { + dirMode = 0555 + format = tar.FormatPAX + paxRecords = map[string]string{ + "MSWINDOWS.rawsd": windowsUserOwnerAndGroupSID, + } + + if err := writeHeader(true, "Files", 0, dirMode); err != nil { + return fail(err) + } + alreadyCreatedDirectories["Files"] = struct{}{} + if err := writeHeader(true, "Hives", 0, dirMode); err != nil { + return fail(err) + } + alreadyCreatedDirectories["Hives"] = struct{}{} + + adjustTargetFilename = func(v string) string { + // At Windows, we need to always use /, because of the TAR format. + // ...and they need to start always with "Files/" instead of "C:\" or similar... + v = strings.ReplaceAll(v, "\\", "/") + if len(v) > 3 && (v[0] == 'C' || v[0] == 'c') && v[1] == ':' && v[2] == '/' { + v = "Files/" + v[3:] + } + return v + } + } + + addItem := func(item imageArtifactLayerItem) (rErr error) { + f, err := gos.Open(item.sourceFile) + if err != nil { + return fail(err) + } + defer common.KeepCloseError(&rErr, f) + fi, err := f.Stat() + if err != nil { + return fail(err) + } + + targetFile := adjustTargetFilename(item.targetFile) + + var directoriesToCreate []string + currentPath := path.Dir(targetFile) + for currentPath != "." && currentPath != "" { + if _, ok := alreadyCreatedDirectories[currentPath]; !ok { + directoriesToCreate = append(directoriesToCreate, currentPath) + alreadyCreatedDirectories[currentPath] = struct{}{} + } + currentPath = path.Dir(currentPath) + } + slices.Reverse(directoriesToCreate) + for _, dir := range directoriesToCreate { + if err := writeHeader(true, dir, 0, dirMode); err != nil { + return fail(err) + } + } + + if err := writeHeader(false, targetFile, fi.Size(), int64(item.mode)); err != nil { + return fail(err) + } + _, err = io.Copy(tw, f) + + return err + } + + for item, err := range items { + if err != nil { + return fail(err) + } + + if err := addItem(item); err != nil { + return failf("cannot add item %q -> %q: %w", item.sourceFile, item.targetFile, err) + } + } + + if err := tw.Flush(); err != nil { + return fail(err) + } + + return nil +} + +type buildImageLayer struct { + bufferFilename string + layer v1.Layer +} + +func (this *buildImageLayer) open() (io.ReadCloser, error) { + return gos.Open(this.bufferFilename) +} + +func (this *buildImageLayer) Close() error { + err := gos.Remove(this.bufferFilename) + if sys.IsNotExist(err) { + return nil + } + return err +} diff --git a/cmd/build/build-image.go b/cmd/build/build-image.go new file mode 100644 index 0000000..ceef5d7 --- /dev/null +++ b/cmd/build/build-image.go @@ -0,0 +1,441 @@ +package main + +import ( + "context" + "fmt" + "slices" + "strings" + "time" + + "github.com/alecthomas/kingpin" + "github.com/echocat/slf4g" + "github.com/echocat/slf4g/fields" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + gcv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-github/v65/github" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/engity-com/bifroest/pkg/common" +) + +const ( + ImageAnnotationEdition = "org.engity.bifroest.edition" + ImageAnnotationPlatform = "org.engity.bifroest.platform" +) + +func newBuildImage(b *build) *buildImage { + return &buildImage{ + build: b, + + dummyConfiguration: "cmd/build/contrib/dummy-for-oci-images.yaml", + defaultConfiguration: "contrib/configurations/sshd-dropin-replacement.yaml", + } +} + +type buildImage struct { + *build + + dummyConfiguration string + defaultConfiguration string +} + +func (this *buildImage) attach(cmd *kingpin.CmdClause) { + cmd.Flag("dummyConfiguration", ""). + Default(this.dummyConfiguration). + PlaceHolder("<file>"). + StringVar(&this.dummyConfiguration) + cmd.Flag("defaultConfiguration", ""). + Default(this.defaultConfiguration). + PlaceHolder("<file>"). + StringVar(&this.defaultConfiguration) +} + +func (this *buildImage) create(ctx context.Context, binary *buildArtifact) (_ buildArtifacts, rErr error) { + var result buildArtifacts + + success := false + a, err := this.createPart(ctx, binary) + if err != nil { + return nil, err + } + defer common.IgnoreCloseErrorIfFalse(&success, a) + result = append(result, a) + + success = true + return result, nil +} + +func (this *buildImage) createPart(ctx context.Context, binary *buildArtifact) (_ *buildArtifact, rErr error) { + success := false + a, err := this.build.newBuildArtifact(ctx, binary.platform, buildArtifactTypeImage) + if err != nil { + return nil, err + } + defer common.IgnoreCloseErrorIfFalse(&success, a) + + fail := func(err error) (*buildArtifact, error) { + return nil, fmt.Errorf("cannot create %v: %w", a, err) + } + + var from string + if from, err = a.from(); err != nil { + return fail(err) + } + + l := log.With("platform", a.platform). + With("from", from). + With("stage", buildStageImage) + + start := time.Now() + l.Debug("building image...") + + ociPlatform, err := gcv1.ParsePlatform(a.ociString()) + if err != nil { + return fail(err) + } + + var img gcv1.Image + if strings.EqualFold(from, "scratch") { + img = empty.Image + img = mutate.MediaType(img, types.OCIManifestSchema1) + img = mutate.ConfigMediaType(img, types.OCIConfigJSON) + } else { + if img, err = crane.Pull(from, + crane.WithPlatform(ociPlatform), + crane.WithContext(ctx), + ); err != nil { + return fail(err) + } + } + + deps, err := this.dependencies.imagesFiles.downloadFilesFor(ctx, a.os, a.arch) + if err != nil { + return fail(err) + } + artifactDepItems := make([]imageArtifactLayerItem, len(deps)) + for i, dep := range deps { + artifactDepItems[i] = imageArtifactLayerItem{ + sourceFile: dep.source, + targetFile: dep.target, + mode: dep.mode, + } + } + + cfg, err := img.ConfigFile() + if err != nil { + return fail(err) + } + cfg = cfg.DeepCopy() + cfg.Architecture = ociPlatform.Architecture + cfg.OS = ociPlatform.OS + cfg.OSVersion = ociPlatform.OSVersion + cfg.OSFeatures = ociPlatform.OSFeatures + cfg.Variant = ociPlatform.Variant + + cfg.Config.Labels = make(map[string]string) + cfg.Config.Env = binary.os.extendPathWith(binary.platform.os.bifroestBinaryDirPath(), cfg.Config.Env) + cfg.Config.Entrypoint = []string{binary.platform.os.bifroestBinaryFilePath()} + cfg.Config.Cmd = []string{"run"} + cfg.Config.ExposedPorts = map[string]struct{}{ + "22/tcp": {}, + } + + img, err = mutate.ConfigFile(img, cfg) + if err != nil { + return fail(err) + } + + annotations, err := this.createAnnotations(ctx, a.edition, func(v version, rm *github.Repository, m map[string]string) error { + m[ImageAnnotationPlatform] = a.platform.String() + return nil + }) + if err != nil { + return fail(err) + } + + img = mutate.Annotations(img, annotations).(gcv1.Image) + + artifacts := []imageArtifactLayerItem{{ + sourceFile: this.defaultConfiguration, + targetFile: binary.platform.os.bifroestConfigFilePath(), + mode: 0644, + }} + + if !a.os.isUnix() || strings.EqualFold(from, "scratch") { + artifacts = []imageArtifactLayerItem{{ + sourceFile: this.dummyConfiguration, + targetFile: binary.platform.os.bifroestConfigFilePath(), + mode: 0644, + }} + } + + if a.os.isUnix() && strings.EqualFold(from, "scratch") { + artifacts = append(artifacts, imageArtifactLayerItem{ + sourceFile: "cmd/build/contrib/passwd", + targetFile: "/etc/passwd", + mode: 0644, + }, imageArtifactLayerItem{ + sourceFile: "cmd/build/contrib/group", + targetFile: "/etc/group", + mode: 0644, + }, imageArtifactLayerItem{ + sourceFile: "cmd/build/contrib/shadow", + targetFile: "/etc/shadow", + mode: 0600, + }) + } + + binaryLayer, err := binary.toLayer(common.JoinSeq2[imageArtifactLayerItem, error]( + common.Seq2ErrOf[imageArtifactLayerItem](artifacts...), + common.Seq2ErrOf[imageArtifactLayerItem](artifactDepItems...), + )) + if err != nil { + return fail(err) + } + + if img, err = mutate.AppendLayers(img, binaryLayer); err != nil { + return fail(err) + } + + a.ociImage = img + + ld := l.With("duration", time.Since(start).Truncate(time.Millisecond)) + if l.IsDebugEnabled() { + ld.Debug("building image... DONE!") + } else { + ld.Info("image built") + } + + success = true + return a, nil +} + +func (this *buildImage) merge(ctx context.Context, as buildArtifacts) (_ buildArtifacts, rErr error) { + result := slices.Collect(as.withoutType(buildArtifactTypeImage)) + + success := false + for _, e := range allEditionVariants { + a, err := this.createdMerged(ctx, e, as) + if err != nil { + return nil, err + } + defer common.IgnoreCloseErrorIfFalse(&success, a) + if a != nil { + result = append(result, a) + } + } + + success = true + return result, nil +} + +func (this *buildImage) createAnnotations(ctx context.Context, e edition, additional func(version, *github.Repository, map[string]string) error) (map[string]string, error) { + rm, err := this.repo.meta(ctx) + if err != nil { + return nil, err + } + + ver, err := this.version(ctx) + if err != nil { + return nil, err + } + commit, err := this.commit(ctx) + if err != nil { + return nil, err + } + + result := map[string]string{ + v1.AnnotationCreated: this.time().Format(time.RFC3339), + v1.AnnotationURL: rm.GetHTMLURL() + "/pkgs/container/" + this.repo.name.String(), + v1.AnnotationDocumentation: rm.GetHomepage(), + v1.AnnotationSource: rm.GetHTMLURL(), + v1.AnnotationVersion: ver.String(), + v1.AnnotationRevision: commit, + v1.AnnotationVendor: this.vendor, + v1.AnnotationTitle: this.title, + v1.AnnotationDescription: rm.GetDescription(), + ImageAnnotationEdition: e.String(), + } + + if l := rm.GetLicense(); l != nil { + result[v1.AnnotationLicenses] = l.GetSPDXID() + } + + for tag := range ver.tags(e.String()+"-", e.String()) { + result[v1.AnnotationRefName] = tag + break + } + + if additional != nil { + if err := additional(ver, rm, result); err != nil { + return nil, err + } + } + + return result, err +} + +func (this *buildImage) createdMerged(ctx context.Context, e edition, as buildArtifacts) (result *buildArtifact, _ error) { + l := log.With("edition", e). + With("stage", buildStageImage) + + start := time.Now() + l.Debug("merge images...") + + annotations, err := this.createAnnotations(ctx, e, nil) + if err != nil { + return nil, err + } + + var manifest gcv1.ImageIndex = empty.Index + manifest = mutate.IndexMediaType(manifest, types.DockerManifestList) + manifest = mutate.Annotations(manifest, annotations).(gcv1.ImageIndex) + + var adds []mutate.IndexAddendum + var refA *buildArtifact + + for aa := range as.filter(func(candidate *buildArtifact) bool { + return candidate.edition == e && candidate.t == buildArtifactTypeImage + }) { + fail := func(err error) (*buildArtifact, error) { + return nil, fmt.Errorf("cannot merge artifact %v: %w", aa, err) + } + + cf, err := aa.ociImage.ConfigFile() + if err != nil { + return fail(err) + } + + newDesc, err := partial.Descriptor(aa.ociImage) + if err != nil { + return fail(err) + } + newDesc.Platform = cf.Platform() + adds = append(adds, mutate.IndexAddendum{ + Add: aa.ociImage, + Descriptor: *newDesc, + }) + refA = aa + } + + success := false + if refA != nil { + result = &buildArtifact{ + platform: &platform{ + edition: e, + testing: refA.testing, + }, + buildContext: refA.buildContext, + t: buildArtifactTypeImagePlatform, + ociIndex: mutate.AppendManifests(manifest, adds...), + } + defer common.IgnoreCloseErrorIfFalse(&success, result) + } + + ld := l.With("duration", time.Since(start).Truncate(time.Millisecond)) + if l.IsDebugEnabled() { + if result != nil { + ld.Debug("merge images... DONE!") + } else { + ld.Debug("merge images... SKIPPED! (none found)") + } + } else if result != nil { + ld.Info("images merged") + } + success = true + return result, nil +} + +func (this *buildImage) publish(ctx context.Context, as buildArtifacts) (rErr error) { + fail := func(err error) error { + return fmt.Errorf("cannot publish artifacts: %w", err) + } + + for a := range as.onlyOfType(buildArtifactTypeImagePlatform) { + l := log.With("edition", a.edition). + With("stage", buildStagePublish) + + start := time.Now() + l.Debug("push images...") + + refs, err := this.refs(ctx, a.edition) + if err != nil { + return fail(err) + } + l = l.With("refs", this.lazyRefs(&refs)) + + for _, ref := range refs { + if err := remote.WriteIndex(ref, a.ociIndex, + remote.WithContext(ctx), + remote.WithAuth(&authn.Basic{ + Username: this.actor, + Password: this.repo.githubToken, + }), + ); err != nil { + return fail(err) + } + } + + ld := l.With("duration", time.Since(start).Truncate(time.Millisecond)) + if l.IsDebugEnabled() { + ld.Debug("push images... DONE!") + } else { + ld.Info("images pushed") + } + } + + return nil +} + +func (this *buildImage) refs(ctx context.Context, e edition) ([]name.Reference, error) { + v, err := this.version(ctx) + if err != nil { + return nil, err + } + + var rs []name.Reference + prefix := e.String() + "-" + root := e.String() + for tag := range v.tags(prefix, root) { + r, err := name.ParseReference(this.repo.fullImageName() + ":" + tag) + if err != nil { + return nil, err + } + rs = append(rs, r) + } + + if e == editionGeneric { + for tag := range v.tags("", "latest") { + r, err := name.ParseReference(this.repo.fullImageName() + ":" + tag) + if err != nil { + return nil, err + } + rs = append(rs, r) + } + } + + return rs, nil +} + +func (this *buildImage) lazyRefs(p *[]name.Reference) fields.Lazy { + return fields.LazyFunc(func() any { + if p == nil || len(*p) == 0 { + return fields.Exclude + } + result := make([]string, len(*p)) + for i, r := range *p { + result[i] = r.String() + } + if len(result) == 1 { + return result[0] + } + return result + }) +} diff --git a/cmd/build/build-stage.go b/cmd/build/build-stage.go new file mode 100644 index 0000000..82fca9e --- /dev/null +++ b/cmd/build/build-stage.go @@ -0,0 +1,108 @@ +package main + +import ( + "fmt" + "slices" + "sort" + "strings" +) + +type buildStage uint8 + +const ( + buildStageBinary buildStage = iota + buildStageArchive + buildStageImage + buildStageDigest + buildStagePublish +) + +func (this buildStage) String() string { + v, ok := buildStageToString[this] + if !ok { + return fmt.Sprintf("illegal-build-stage-%d", this) + } + return v +} + +func (this *buildStage) Set(plain string) error { + v, ok := stringToBuildStage[plain] + if !ok { + return fmt.Errorf("illegal-build-stage: %s", plain) + } + *this = v + return nil +} + +type buildStages []buildStage + +func (this buildStages) contains(v buildStage) bool { + return slices.Contains(this, v) +} + +func (this buildStages) filter(predicate func(buildStage) bool) buildStages { + var result buildStages + for _, v := range this { + if predicate(v) { + result = append(result, v) + } + } + return result +} + +func (this buildStages) String() string { + return strings.Join(this.Strings(), ",") +} + +func (this buildStages) Strings() []string { + strs := make([]string, len(this)) + for i, v := range this { + strs[i] = v.String() + } + return strs +} + +func (this *buildStages) Set(plain string) error { + parts := strings.Split(plain, ",") + buf := make(buildStages, len(parts)) + for i, part := range parts { + part = strings.TrimSpace(part) + if err := buf[i].Set(part); err != nil { + return err + } + } + sort.Sort(buf) + *this = buf + return nil +} + +func (x buildStages) Len() int { return len(x) } +func (x buildStages) Less(i, j int) bool { return x[i] < x[j] } +func (x buildStages) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +var ( + buildStageToString = map[buildStage]string{ + buildStageBinary: "binary", + buildStageArchive: "archive", + buildStageImage: "image", + buildStageDigest: "digest", + buildStagePublish: "publish", + } + stringToBuildStage = func(in map[buildStage]string) map[string]buildStage { + result := make(map[string]buildStage, len(in)) + for k, v := range in { + result[v] = k + } + return result + }(buildStageToString) + allBuildStageVariants = func(in map[buildStage]string) buildStages { + result := make(buildStages, len(in)) + var i int + for k := range in { + result[i] = k + i++ + } + sort.Sort(result) + return result + }(buildStageToString) +) diff --git a/cmd/build/build.go b/cmd/build/build.go new file mode 100644 index 0000000..f79b620 --- /dev/null +++ b/cmd/build/build.go @@ -0,0 +1,548 @@ +package main + +import ( + "context" + "fmt" + "iter" + gos "os" + "path/filepath" + "runtime" + "slices" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/alecthomas/kingpin" + "github.com/echocat/slf4g" + + "github.com/engity-com/bifroest/pkg/common" +) + +func newBuild(b *base) *build { + result := &build{ + base: b, + + vendor: "unknown", + dest: "var/dist", + prefix: "bifroest", + rawStages: nil, + oses: allOsVariants, + archs: allArchVariants, + editions: allEditionVariants, + testing: false, + + updateCaCerts: true, + wslBuildDistribution: "", + } + result.binary = newBuildBinary(result) + result.archive = newBuildArchive(result) + result.image = newBuildImage(result) + result.digest = newBuildDigest(result) + return result +} + +type build struct { + *base + binary *buildBinary + archive *buildArchive + image *buildImage + digest *buildDigest + + vendor string + dest string + prefix string + rawStages buildStages + oses oses + archs archs + editions editions + testing bool + + updateCaCerts bool + wslBuildDistribution string + + timeP atomic.Pointer[time.Time] + buildContextP atomic.Pointer[buildContext] + stagesP atomic.Pointer[buildStages] +} + +func (this *build) init(ctx context.Context, app *kingpin.Application) { + attach := func(cmd *kingpin.CmdClause) { + cmd.Flag("vendor", ""). + Envar("BIFROEST_VENDOR"). + PlaceHolder("<name>"). + StringVar(&this.vendor) + cmd.Flag("dest", ""). + Default(this.dest). + PlaceHolder("<path>"). + StringVar(&this.dest) + cmd.Flag("prefix", ""). + PlaceHolder("<prefix>"). + Default(this.prefix). + StringVar(&this.prefix) + cmd.Flag("stages", ""). + PlaceHolder("<" + strings.Join(allBuildStageVariants.Strings(), "|") + ">[,...]"). + SetValue(&this.rawStages) + cmd.Flag("os", ""). + PlaceHolder("<" + strings.Join(allOsVariants.Strings(), "|") + ">[,...]"). + Default(this.oses.String()). + SetValue(&this.oses) + cmd.Flag("arch", ""). + PlaceHolder("<" + strings.Join(allArchVariants.Strings(), "|") + ">[,...]"). + Default(this.archs.String()). + SetValue(&this.archs) + cmd.Flag("edition", ""). + PlaceHolder("<" + strings.Join(allEditionVariants.Strings(), "|") + ">[,...]"). + Default(this.editions.String()). + SetValue(&this.editions) + cmd.Flag("testing", ""). + BoolVar(&this.testing) + cmd.Flag("updateCaCerts", ""). + BoolVar(&this.updateCaCerts) + + cmd.Flag("wslBuildDistribution", ""). + PlaceHolder("<distroName>"). + Default(this.wslBuildDistribution). + StringVar(&this.wslBuildDistribution) + + this.binary.attach(cmd) + this.archive.attach(cmd) + this.image.attach(cmd) + this.digest.attach(cmd) + } + + attach(app.Command("evaluate-environment", ""). + Action(func(*kingpin.ParseContext) error { + return this.evaluateEnvironment(ctx) + })) + + attach(app.Command("build", ""). + Action(func(*kingpin.ParseContext) (rErr error) { + as, err := this.buildAll(ctx, this.testing) + if err != nil { + return err + } + defer common.KeepCloseError(&rErr, as) + + return nil + })) +} + +func (this *build) allPlatforms(forTesting bool) iter.Seq[*platform] { + return func(yield func(*platform) bool) { + for p := range allBinaryPlatforms(forTesting, this.assumedBuildOs(), this.assumedBuildArch()) { + if slices.Contains(this.oses, p.os) && + slices.Contains(this.archs, p.arch) && slices.Contains(this.editions, p.edition) { + if !yield(p) { + return + } + } + } + } +} + +func (this *build) evaluateEnvironment(ctx context.Context) error { + commit, err := this.commit(ctx) + if err != nil { + return err + } + ref, err := this.ref(ctx) + if err != nil { + return err + } + ver, err := this.version(ctx) + if err != nil { + return err + } + pr := this.pr() + stages, err := this.stages(ctx) + if err != nil { + return err + } + + log.With("commit", commit). + With("version", ver). + With("ref", ref). + With("pr", pr). + With("stages", stages). + Info() + prStr := strconv.FormatUint(uint64(pr), 10) + + if fn := this.optionsOutputFilename; fn != "" { + f, err := gos.OpenFile(fn, gos.O_CREATE|gos.O_APPEND|gos.O_WRONLY, 0644) + if err != nil { + return err + } + defer common.IgnoreCloseError(f) + + if _, err = fmt.Fprint(f, ""+ + "commit="+commit+"\n"+ + "version="+ver.String()+"\n"+ + "ref="+ref+"\n"+ + "pr="+prStr+"\n", + ); err != nil { + return err + } + + for _, stage := range allBuildStageVariants { + if _, err = fmt.Fprintf(f, "stage-%v=%v\n", stage, stages.contains(stage)); err != nil { + return err + } + } + + log.With("file", fn). + Info("options output created") + } + + if fn := this.summaryOutputFilename; fn != "" { + f, err := gos.OpenFile(fn, gos.O_CREATE|gos.O_APPEND|gos.O_WRONLY, 0644) + if err != nil { + return err + } + defer common.IgnoreCloseError(f) + + baseUrl := "https://" + this.repo.fullName() + + if _, err = fmt.Fprint(f, "## Build environment\n"+ + "| Name | Value |\n"+ + "| ---- | ----- |\n"+ + "| Commit | [`"+commit+"`]("+baseUrl+"/commit/"+commit+") |\n"+ + "| Version | `"+ver.String()+"` |\n"+ + "| Ref | [`"+ref+"`]("+baseUrl+"/tree/"+ref+") |\n"+ + "| PR | [`"+prStr+"`]("+baseUrl+"/pull/"+prStr+") |\n"+ + "\n", + "## Available stages\n"+ + "| Name | Enabled |\n"+ + "| ---- | ------- |\n", + ); err != nil { + return err + } + + for _, stage := range allBuildStageVariants { + if _, err = fmt.Fprintf(f, "| `%v` | `%v` |\n", stage, stages.contains(stage)); err != nil { + return err + } + } + + log.With("file", fn). + Info("summary output created") + } + + return nil +} + +func (this *build) buildAll(ctx context.Context, forTesting bool) (artifacts buildArtifacts, _ error) { + stages, err := this.stages(ctx) + if err != nil { + return nil, err + } + + if this.updateCaCerts { + if err := this.dependencies.caCerts.generatePem(ctx); err != nil { + return nil, err + } + } + success := false + defer common.IgnoreCloseErrorIfFalse(&success, artifacts) + + for a := range this.allPlatforms(forTesting) { + vs, err := this.buildSingle(ctx, a) + if err != nil { + return nil, err + } + artifacts = append(artifacts, vs...) + } + + if stages.contains(buildStageImage) { + var err error + artifacts, err = this.image.merge(ctx, artifacts) + if err != nil { + return nil, err + } + } + + if stages.contains(buildStageDigest) { + var err error + artifacts, err = this.digest.create(ctx, artifacts) + if err != nil { + return nil, err + } + } + + if stages.contains(buildStagePublish) { + if err := this.publish(ctx, artifacts); err != nil { + return nil, err + } + } + + success = true + return artifacts, nil +} + +func (this *build) buildSingle(ctx context.Context, p *platform) (artifacts buildArtifacts, _ error) { + fail := func(err error) ([]*buildArtifact, error) { + return nil, fmt.Errorf("cannot build %v: %w", *p, err) + } + + stages, err := this.stages(ctx) + if err != nil { + return nil, err + } + + l := log.With("platform", p) + + success := false + common.IgnoreCloseErrorIfFalse(&success, artifacts) + + var ba *buildArtifact + if stages.contains(buildStageBinary) && p.isBinarySupported(this.assumedBuildOs(), this.assumedBuildArch()) { + var err error + ba, err = this.binary.compile(ctx, p) + if err != nil { + return fail(err) + } + artifacts = append(artifacts, ba) + + } else { + l.With("stage", buildStageBinary).Info("build binary skipped") + } + + if ba != nil && stages.contains(buildStageArchive) { + aa, err := this.archive.create(ctx, ba) + if err != nil { + return fail(err) + } + artifacts = append(artifacts, aa) + } else { + l.With("stage", buildStageArchive).Info("build archive skipped") + } + + if ba != nil && stages.contains(buildStageImage) && ba.isImageSupported() { + aas, err := this.image.create(ctx, ba) + if err != nil { + return fail(err) + } + artifacts = append(artifacts, aas...) + } else { + l.With("stage", buildStageImage).Info("build image skipped") + } + + success = true + return artifacts, nil +} + +func (this *build) publish(ctx context.Context, as buildArtifacts) error { + fail := func(err error) error { + return fmt.Errorf("cannot publish: %w", err) + } + + if err := this.image.publish(ctx, as); err != nil { + return fail(err) + } + + release, err := this.repo.releases.findCurrent(ctx) + if err != nil { + return fail(err) + } + + if release == nil { + log.Info("outside of release; publish artifacts skipped") + return nil + } + + l := log.With("release", release) + + start := time.Now() + l.Debug("publish release...") + + for a := range as.filter(func(candidate *buildArtifact) bool { + return candidate.t.canBePublished() + }) { + if _, err := release.uploadAsset(ctx, a.name(), a.mediaType(), "", a.filepath); err != nil { + return fail(err) + } + } + + ll := l.With("duration", time.Since(start).Truncate(time.Millisecond)) + if l.IsDebugEnabled() { + ll.Info("publish release...DONE!") + } else { + ll.Info("release published") + } + + return nil +} + +func (this *build) time() time.Time { + for { + if v := this.timeP.Load(); v != nil { + return *v + } + v := time.Now() + if this.timeP.CompareAndSwap(nil, &v) { + return v + } + runtime.Gosched() + } +} + +func (this *build) getBuildContext(ctx context.Context) (*buildContext, error) { + for { + if v := this.buildContextP.Load(); v != nil { + return v, nil + } + versions, err := this.version(ctx) + if err != nil { + return nil, err + } + revision, err := this.commit(ctx) + if err != nil { + return nil, err + } + + v := &buildContext{ + this, + versions, + this.time(), + this.vendor, + revision, + } + if this.buildContextP.CompareAndSwap(nil, v) { + return v, nil + } + runtime.Gosched() + } +} + +func (this *build) newBuildFileArtifact(ctx context.Context, p *platform, t buildArtifactType, fn string) (*buildArtifact, error) { + success := false + result, err := this.newBuildArtifact(ctx, p, t) + if err != nil { + return nil, err + } + defer common.IgnoreCloseErrorIfFalse(&success, result) + + result.filepath = result.buildContext.filepath(fn) + + success = true + return result, nil +} + +func (this *build) newBuildArtifact(ctx context.Context, p *platform, t buildArtifactType) (*buildArtifact, error) { + bc, err := this.getBuildContext(ctx) + if err != nil { + return nil, err + } + + return &buildArtifact{ + platform: p, + buildContext: bc, + t: t, + }, nil +} + +func (this *build) assumedBuildOs() os { + if goos == osWindows && this.wslBuildDistribution != "" { + return osLinux + } + return goos +} + +func (this *build) assumedBuildArch() arch { + return goarch +} + +func (this *build) translateToWslPath(in string) (string, error) { + if goos != osWindows { + return "", fmt.Errorf("can only translate %q to wsl path if os is: %v; but is: %v", in, osWindows, goos) + } + abs, err := filepath.Abs(in) + if err != nil { + return "", err + } + + return "/mnt/" + strings.ToLower(abs[0:1]) + filepath.ToSlash(abs[2:]), nil +} + +func (this *build) stages(ctx context.Context) (buildStages, error) { + for { + v := this.stagesP.Load() + if v != nil { + return *v, nil + } + nv, err := this.resolveStages(ctx) + if err != nil { + return nil, err + } + if this.stagesP.CompareAndSwap(nil, &nv) { + return nv, nil + } + runtime.Gosched() + } +} + +func (this *build) resolveStages(ctx context.Context) (buildStages, error) { + if v := this.rawStages; len(v) > 0 { + return v, nil + } + + ver, err := this.version(ctx) + if err != nil { + return nil, err + } + + // Assume release... + if ver.semver != nil { + log.With("version", ver). + Infof("as this is a release version; stage %v was implicitly enabled", buildStagePublish) + return allBuildStageVariants, nil + } + + // Check if the PR is allowed to have images... + if v := this.pr(); v > 0 { + pr, err := this.repo.prs.byId(ctx, v) + if err != nil { + return nil, err + } + // Ok, in this case allow images... + if pr.isOpen() && pr.hasLabel(this.repo.prs.testPublishLabel) { + log.With("pr", v). + Infof("as this is a PR and it was the label %v; stage %v was implicitly enabled", this.repo.prs.testPublishLabel, buildStagePublish) + return allBuildStageVariants, nil + } + } + + // Never publish this states... + return allBuildStageVariants.filter(func(v buildStage) bool { + return v != buildStagePublish + }), nil + +} + +type buildContext struct { + build *build + + version version + time time.Time + vendor string + revision string +} + +func (this buildContext) filepath(fn string) string { + dir := filepath.Join(this.build.dest, this.version.String()) + _ = gos.MkdirAll(dir, 0755) + return filepath.Join(dir, fn) +} + +func (this buildContext) toLdFlags(testing bool) string { + testPrefix := "" + testSuffix := "" + if testing { + testPrefix = "TEST" + testSuffix = "TEST" + } + return "-X main.version=" + testPrefix + this.version.String() + testSuffix + + " -X main.revision=" + this.revision + + " -X " + strconv.Quote("main.vendor="+this.vendor) + + " -X main.buildAt=" + this.time.Format(time.RFC3339) +} diff --git a/cmd/build/contrib/dummy-for-oci-images.yaml b/cmd/build/contrib/dummy-for-oci-images.yaml new file mode 100644 index 0000000..904c605 --- /dev/null +++ b/cmd/build/contrib/dummy-for-oci-images.yaml @@ -0,0 +1,31 @@ +## This configuration should only be used as dummy configuration within OCI/Docker images. +## It will create (if not exists) +## for the regular sshd. + +startMessage: > + Welcome to Engity's Bifröst! + This instance is running a demo configuration that is NOT + intended for production use. Therefore, it is not possible to + log in to this instance until it is configured. + See https://bifroest.engity.org/latest/setup/ for more details. + +ssh: + addresses: [ ":22" ] + banner: |+ + Transcend with Engity's Bifröst + =============================== + + This instance is running a demo configuration that is NOT + intended for production use. Therefore, it is not possible to + log in to this instance until it is configured. + + See https://bifroest.engity.org/latest/setup/ for more details. + +flows: + - name: default + authorization: + type: htpasswd + environment: + type: local + # This property will not be evaluated in Windows. + name: "demo" diff --git a/cmd/build/contrib/group b/cmd/build/contrib/group new file mode 100644 index 0000000..1dbf901 --- /dev/null +++ b/cmd/build/contrib/group @@ -0,0 +1 @@ +root:x:0: diff --git a/cmd/build/contrib/passwd b/cmd/build/contrib/passwd new file mode 100644 index 0000000..6d9b028 --- /dev/null +++ b/cmd/build/contrib/passwd @@ -0,0 +1 @@ +root:x:0:0:root:/:/usr/bin/bifroest diff --git a/cmd/build/contrib/shadow b/cmd/build/contrib/shadow new file mode 100644 index 0000000..cafaee5 --- /dev/null +++ b/cmd/build/contrib/shadow @@ -0,0 +1 @@ +root:*:19683:0:99999:7::: diff --git a/cmd/build/dependencies-ca-certs.go b/cmd/build/dependencies-ca-certs.go new file mode 100644 index 0000000..a415bf8 --- /dev/null +++ b/cmd/build/dependencies-ca-certs.go @@ -0,0 +1,130 @@ +package main + +import ( + "context" + "encoding/pem" + "fmt" + "io" + "net/http" + gos "os" + "path/filepath" + "time" + + "github.com/alecthomas/kingpin" + log "github.com/echocat/slf4g" + "github.com/gwatts/rootcerts/certparse" + + "github.com/engity-com/bifroest/pkg/common" +) + +const ( + certdataDownloadUrl = "https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt" +) + +var ( + defaultCaCertsTargetFn = filepath.Join("pkg", "crypto", "ca-certs.crt") +) + +func newDependenciesCaCerts(b *dependencies) *dependenciesCaCerts { + return &dependenciesCaCerts{ + dependencies: b, + + sourceUrl: certdataDownloadUrl, + targetFile: defaultCaCertsTargetFn, + } +} + +type dependenciesCaCerts struct { + dependencies *dependencies + + sourceUrl string + targetFile string +} + +func (this *dependenciesCaCerts) init(ctx context.Context, app *kingpin.Application) { + cmd := app.Command("ca-certs", "") + + app.Flag("caCertsUrl", ""). + Default(this.sourceUrl). + StringVar(&this.sourceUrl) + app.Flag("caCertsTargetPemFile", ""). + Default(this.targetFile). + StringVar(&this.targetFile) + + cmdPem := cmd.Command("pem", "") + cmdPem.Action(func(*kingpin.ParseContext) error { + return this.generatePem(ctx) + }) +} + +func (this *dependenciesCaCerts) generatePem(ctx context.Context) (rErr error) { + var f *gos.File + if this.targetFile == "stdout" { + f = gos.Stdout + } else { + var err error + if f, err = gos.OpenFile(this.targetFile, gos.O_CREATE|gos.O_WRONLY|gos.O_TRUNC, 0644); err != nil { + return err + } + defer common.KeepCloseError(&rErr, f) + } + + return this.generate(ctx, f) +} + +func (this *dependenciesCaCerts) generate(ctx context.Context, to io.Writer) error { + fail := func(err error) error { + return fmt.Errorf("cannot generate ca-certs from %s: %w", this.sourceUrl, err) + } + failf := func(msg string, args ...any) error { + return fail(fmt.Errorf(msg, args...)) + } + + start := time.Now() + l := log.With("source", this.sourceUrl) + + l.Debug("downloading ca-certs...") + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, this.sourceUrl, nil) + if err != nil { + return fail(err) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fail(err) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return failf("illegal response code: %d", resp.StatusCode) + } + defer common.IgnoreCloseError(resp.Body) + + certs, err := certparse.ReadTrustedCerts(resp.Body) + if err != nil { + return fail(err) + } + + for _, cert := range certs { + if err := ctx.Err(); err != nil { + return err + } + if cert.Trust != certparse.ServerTrustedDelegator { + continue + } + + if err := pem.Encode(to, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Data, + }); err != nil { + return fail(err) + } + } + + l = l.With("duration", time.Since(start).Truncate(time.Millisecond)) + if l.IsDebugEnabled() { + l.Info("downloading ca-certs... DONE!") + } else { + l.Info("ca-certs downloaded") + } + + return nil +} diff --git a/cmd/build/dependencies-images-files.go b/cmd/build/dependencies-images-files.go new file mode 100644 index 0000000..5a2f668 --- /dev/null +++ b/cmd/build/dependencies-images-files.go @@ -0,0 +1,217 @@ +package main + +import ( + "archive/tar" + "context" + "crypto/sha1" + "fmt" + "io" + gos "os" + "path/filepath" + "strings" + "time" + + "github.com/alecthomas/kingpin" + log "github.com/echocat/slf4g" + "github.com/google/go-containerregistry/pkg/crane" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/mr-tron/base58" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/sys" +) + +const ( + netapi32DllFilename = `C:\Windows\System32\netapi32.dll` +) + +func newDependenciesImagesFiles(b *dependencies) *dependenciesImagesFiles { + return &dependenciesImagesFiles{ + dependencies: b, + cacheDirectory: filepath.Join(".cache", "dependencies", "images"), + imageWithNetapi32Dll: "mcr.microsoft.com/windows/servercore:ltsc2022", + } +} + +type dependenciesImagesFiles struct { + dependencies *dependencies + + cacheDirectory string + alwaysRefresh bool + imageWithNetapi32Dll string +} + +func (this *dependenciesImagesFiles) init(_ context.Context, app *kingpin.Application) { + app.Flag("dependenciesImagesCacheDirectory", ""). + Default(this.cacheDirectory). + StringVar(&this.cacheDirectory) + app.Flag("dependenciesImagesAlwaysRefresh", ""). + BoolVar(&this.alwaysRefresh) + app.Flag("dependenciesImageWithNetapi32Dll", ""). + Default(this.imageWithNetapi32Dll). + StringVar(&this.imageWithNetapi32Dll) +} + +func (this *dependenciesImagesFiles) downloadFilesFor(ctx context.Context, os os, arch arch) (result []imageFileDependency, err error) { + v, err := this.downloadNetapi32DllFor(ctx, os, arch) + if err != nil { + return nil, err + } + result = append(result, v...) + + return result, nil +} + +func (this *dependenciesImagesFiles) downloadNetapi32DllFor(ctx context.Context, os os, arch arch) ([]imageFileDependency, error) { + fail := func(err error) ([]imageFileDependency, error) { + return nil, fmt.Errorf("cannot download netapi32.dll for %v/%v: %w", os, arch, err) + } + + if os != osWindows { + return nil, nil + } + + targetFn := this.cacheLocationFor(os, arch, "netapi32.dll") + + err := this.getFileFromImage(ctx, os, arch, this.imageWithNetapi32Dll, "Files/Windows/System32/netapi32.dll", targetFn) + if err != nil { + return fail(err) + } + + return []imageFileDependency{{os, arch, targetFn, netapi32DllFilename, 0644}}, nil +} + +func (this *dependenciesImagesFiles) cacheLocationFor(os os, arch arch, base string) string { + hash := sha1.Sum([]byte(base)) + return filepath.Join(this.cacheDirectory, os.String()+"-"+arch.String(), base58.Encode(hash[:]), "netapi32.dll") +} + +func (this *dependenciesImagesFiles) getFileFromImage(ctx context.Context, os os, arch arch, imgName string, sourceFn, targetFn string) (rErr error) { + fail := func(err error) error { + return fmt.Errorf("cannot download %q from %q (%v/%v): %w", sourceFn, imgName, os, arch, err) + } + failf := func(msg string, args ...any) error { + return fail(fmt.Errorf(msg, args...)) + } + + if !this.alwaysRefresh { + efi, err := gos.Stat(targetFn) + if err == nil { + if efi.IsDir() { + return failf("%s is a directory", targetFn) + } + return nil + } + if !sys.IsNotExist(err) { + return fail(err) + } + } + + start := time.Now() + l := log.With("image", imgName). + With("os", os). + With("arch", arch). + With("file", sourceFn) + + l.Debug("download dependency file...") + + _ = gos.MkdirAll(filepath.Dir(targetFn), 0755) + + img, err := crane.Pull(imgName, + crane.WithPlatform(&v1.Platform{ + OS: os.String(), + Architecture: arch.ociString(), + }), + crane.WithContext(ctx), + ) + if err != nil { + return failf("cannot pull image %q: %w", imgName, err) + } + + layers, err := img.Layers() + if err != nil { + return failf("cannot get layers of image %q: %w", imgName, err) + } + + for i, layer := range layers { + ok, err := this.getFileFromLayer(layer, sourceFn, targetFn) + if err != nil { + return failf("layer %d", i, err) + } + if ok { + l = l.With("duration", time.Since(start).Truncate(time.Millisecond)) + + if l.IsDebugEnabled() { + l.Info("download dependency file... DONE!") + } else { + l.Info("dependency file downloaded") + } + + return nil + } + } + + return failf("file does not exist in image") +} + +func (this *dependenciesImagesFiles) getFileFromLayer(layer v1.Layer, sourceFn, targetFn string) (_ bool, rErr error) { + fail := func(err error) (bool, error) { + return false, err + } + failf := func(msg string, args ...any) (bool, error) { + return fail(fmt.Errorf(msg, args...)) + } + + mt, err := layer.MediaType() + if err != nil { + return failf("cannot media type of layer: %w", err) + } + + switch mt { + case types.OCILayer, types.OCILayerZStd, types.OCIUncompressedLayer, types.DockerLayer, types.DockerForeignLayer, types.DockerUncompressedLayer: + default: + return false, nil + } + + r, err := layer.Uncompressed() + if err != nil { + return failf("cannot get uncompressed part of layer: %w", err) + } + defer common.IgnoreCloseError(r) + + tr := tar.NewReader(r) + + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return failf("cannot read TAR from uncompressed part of layer: %w", err) + } + if strings.EqualFold(header.Name, sourceFn) { + to, err := gos.OpenFile(targetFn, gos.O_TRUNC|gos.O_CREATE|gos.O_WRONLY, 0644) + if err != nil { + return failf("cannot create file %q: %w", targetFn, err) + } + defer common.KeepCloseError(&rErr, to) + + if _, err := io.Copy(to, tr); err != nil { + return failf("cannot write file from archive %q to %q: %w", targetFn, err) + } + + return true, nil + } + } + + return false, nil +} + +type imageFileDependency struct { + os os + arch arch + source string + target string + mode gos.FileMode +} diff --git a/cmd/build/dependencies.go b/cmd/build/dependencies.go new file mode 100644 index 0000000..9a467ba --- /dev/null +++ b/cmd/build/dependencies.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + + "github.com/alecthomas/kingpin" +) + +func newDependencies(b *base) *dependencies { + result := &dependencies{ + base: b, + } + + result.caCerts = newDependenciesCaCerts(result) + result.imagesFiles = newDependenciesImagesFiles(result) + + return result +} + +type dependencies struct { + base *base + + caCerts *dependenciesCaCerts + imagesFiles *dependenciesImagesFiles +} + +func (this *dependencies) init(ctx context.Context, app *kingpin.Application) { + this.caCerts.init(ctx, app) + this.imagesFiles.init(ctx, app) +} diff --git a/cmd/build/edition.go b/cmd/build/edition.go new file mode 100644 index 0000000..edc0f6f --- /dev/null +++ b/cmd/build/edition.go @@ -0,0 +1,100 @@ +package main + +import ( + "strings" + + "github.com/engity-com/bifroest/pkg/common" +) + +type edition common.VersionEdition + +func (this edition) String() string { + return common.VersionEdition(this).String() +} + +func (this *edition) Set(plain string) error { + var buf common.VersionEdition + if err := buf.Set(plain); err != nil { + return err + } + *this = edition(buf) + return nil +} + +func (this edition) isBinarySupported(o os, a arch, assumedOs os, assumedArch arch) bool { + if !a.isOsSupported(o) { + return false + } + + if this == editionGeneric { + return true + } + + if this == editionExtended { + if assumedOs == 0 { + assumedOs = goos + } + if assumedArch == 0 { + assumedArch = goarch + } + buildDetails := archToDetails[a].os[o].build[archBuildKey{assumedOs, assumedArch}] + return buildDetails.crossCc != "" + } + + return false +} + +func (this edition) setToEnv(env interface{ setEnv(key, val string) }) { + switch this { + case editionExtended: + env.setEnv("CGO_ENABLED", "1") + default: + env.setEnv("CGO_ENABLED", "0") + } +} + +func (this edition) toLdFlags(_ os) string { + result := "-X main.edition=" + this.String() + switch this { + case editionExtended: + result += " -linkmode external" + } + return result +} + +type editions []edition + +func (this editions) String() string { + return strings.Join(this.Strings(), ",") +} + +func (this editions) Strings() []string { + strs := make([]string, len(this)) + for i, v := range this { + strs[i] = v.String() + } + return strs +} + +func (this *editions) Set(plain string) error { + parts := strings.Split(plain, ",") + buf := make(editions, len(parts)) + for i, part := range parts { + part = strings.TrimSpace(part) + if err := buf[i].Set(part); err != nil { + return err + } + } + *this = buf + return nil +} + +const ( + editionUnknown = edition(common.VersionEditionUnknown) + editionGeneric = edition(common.VersionEditionGeneric) + editionExtended = edition(common.VersionEditionExtended) +) + +var ( + allEditionVariants = editions{editionGeneric, editionExtended} +) diff --git a/cmd/build/exec.go b/cmd/build/exec.go new file mode 100644 index 0000000..bb1447d --- /dev/null +++ b/cmd/build/exec.go @@ -0,0 +1,97 @@ +package main + +import ( + "bytes" + "context" + "fmt" + gos "os" + gexec "os/exec" + "strings" + + "github.com/alecthomas/kingpin" + + "github.com/engity-com/bifroest/pkg/errors" +) + +func newExec(b *base) *exec { + return &exec{ + base: b, + } +} + +type exec struct { + base *base +} + +func (this *exec) init(_ context.Context, _ *kingpin.Application) {} + +func (this *exec) execute(ctx context.Context, cmd string, args ...string) *execCmd { + result := execCmd{ + parent: this, + cmd: gexec.CommandContext(ctx, cmd, args...), + } + + env := gos.Environ() + result.env = make(map[string]string, len(env)) + for _, kv := range env { + parts := strings.SplitN(kv, "=", 2) + result.env[parts[0]] = parts[1] + } + result.cmd.Stdout = &result.stdout + result.cmd.Stderr = &result.stderr + + return &result +} + +type execCmd struct { + parent *exec + cmd *gexec.Cmd + + env map[string]string + stdout bytes.Buffer + stderr bytes.Buffer +} + +func (this *execCmd) wrapError(err error) error { + if err == nil { + return nil + } + return fmt.Errorf("%v: %s", this, err) +} + +func (this *execCmd) String() string { + return this.cmd.String() +} + +func (this *execCmd) setEnv(key, val string) { + this.env[key] = val +} + +func (this *execCmd) attachStd() *execCmd { + this.cmd.Stdin = gos.Stdin + this.cmd.Stdout = gos.Stdout + this.cmd.Stderr = gos.Stderr + return this +} +func (this *execCmd) do() error { + this.cmd.Env = make([]string, len(this.env)) + var i int + for k, v := range this.env { + this.cmd.Env[i] = k + "=" + v + i++ + } + + var eErr *gexec.ExitError + err := this.cmd.Run() + if errors.As(err, &eErr) { + return this.wrapError(fmt.Errorf("%v\n%s", eErr, this.stderr.String())) + } + return err +} + +func (this *execCmd) doAndGet() (string, error) { + if err := this.do(); err != nil { + return "", err + } + return strings.TrimSpace(this.stdout.String()), nil +} diff --git a/cmd/build/main.go b/cmd/build/main.go new file mode 100644 index 0000000..0d913ba --- /dev/null +++ b/cmd/build/main.go @@ -0,0 +1,83 @@ +package main + +import ( + "context" + gos "os" + "os/signal" + "strings" + "syscall" + + "github.com/alecthomas/kingpin" + "github.com/echocat/slf4g" + "github.com/echocat/slf4g/level" + "github.com/echocat/slf4g/native" + "github.com/echocat/slf4g/native/consumer" + "github.com/echocat/slf4g/native/facade/value" + + "github.com/engity-com/bifroest/pkg/common" +) + +func main() { + b := newBase() + + app := kingpin.New("build", "Command used only for building bifroest."). + UsageWriter(gos.Stderr). + ErrorWriter(gos.Stderr). + Terminate(func(i int) { + code := max(i, 1) + gos.Exit(code) + }) + + configureLog(app, native.DefaultProvider) + + ctx, cancelFunc := context.WithCancel(context.Background()) + sigs := make(chan gos.Signal, 1) + defer close(sigs) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigs + cancelFunc() + }() + + b.init(ctx, app) + + if _, err := app.Parse(gos.Args[1:]); err != nil { + log.WithError(err).Error("execution failed") + gos.Exit(1) + } +} + +type logProvider interface { + log.Provider + value.ProviderTarget + level.NamesAware +} + +func configureLog(app *kingpin.Application, of logProvider) { + native.DefaultProvider.Consumer = consumer.NewWriter(gos.Stdout) + + if gos.Getenv("RUNNER_DEBUG") == "1" { + of.SetLevel(level.Debug) + } + + lv := value.NewProvider(of) + app.Flag("log.level", "Defines the minimum level at which the log messages will be logged. Default: "+lv.Level.String()). + PlaceHolder("<" + strings.Join(logLevelStrings(of), "|") + ">"). + SetValue(lv.Level) + app.Flag("log.colorMode", "Tells if to log in color or not. Default: "+lv.Consumer.Formatter.ColorMode.String()). + PlaceHolder("<auto|always|never>"). + SetValue(lv.Consumer.Formatter.ColorMode) +} + +func logLevelStrings(of logProvider) []string { + names := of.GetLevelNames() + + lvls := of.GetAllLevels() + all := make([]string, len(lvls)) + for i, lvl := range lvls { + name, err := names.ToName(lvl) + common.Must(err) + all[i] = name + } + return all +} diff --git a/cmd/build/os.go b/cmd/build/os.go new file mode 100644 index 0000000..acfaa1d --- /dev/null +++ b/cmd/build/os.go @@ -0,0 +1,176 @@ +package main + +import ( + "fmt" + "runtime" + "slices" + "strings" + + "github.com/engity-com/bifroest/pkg/common" +) + +type os uint8 + +const ( + osUnknown os = iota + osLinux os = iota + osWindows +) + +var goos = func() os { + var buf os + common.Must(buf.Set(runtime.GOOS)) + return buf +}() + +func (this os) String() string { + v, ok := osToString[this] + if !ok { + return fmt.Sprintf("illegal-os-%d", this) + } + return v +} + +func (this *os) Set(plain string) error { + v, ok := stringToOs[plain] + if !ok { + return fmt.Errorf("illegal-os: %s", plain) + } + *this = v + return nil +} + +func (this os) execExt() string { + switch this { + case osWindows: + return ".exe" + default: + return "" + } +} + +func (this os) isUnix() bool { + switch this { + case osLinux: + return true + default: + return false + } +} + +func (this os) bifroestBinaryDirPath() string { + switch this { + case osWindows: + return `C:\Program Files\Engity\Bifroest` + default: + return `/usr/bin` + } +} + +func (this os) bifroestBinaryFilePath() string { + switch this { + case osWindows: + return this.bifroestBinaryDirPath() + `\bifroest` + this.execExt() + default: + return this.bifroestBinaryDirPath() + `/bifroest` + this.execExt() + } +} + +func (this os) bifroestConfigDirPath() string { + switch this { + case osWindows: + return `C:\ProgramData\Engity\Bifroest` + default: + return `/etc/engity/bifroest` + } +} + +func (this os) bifroestConfigFilePath() string { + switch this { + case osWindows: + return this.bifroestConfigDirPath() + `\configuration.yaml` + default: + return this.bifroestConfigDirPath() + `/configuration.yaml` + } +} + +func (this os) extendPathWith(dir string, sourceEnv []string) []string { + env := slices.Clone(sourceEnv) + for i, v := range sourceEnv { + if strings.HasPrefix(v, "PATH=") { + switch this { + case osWindows: + env[i] = v + ";" + dir + default: + env[i] = v + ":" + dir + } + return env + } + } + + env = append(env, "PATH="+dir) + return env +} + +func (this os) archiveFormat() archiveFormat { + switch this { + case osWindows: + return packFormatZip + default: + return packFormatTgz + } +} + +func (this os) setToEnv(env interface{ setEnv(key, val string) }) { + env.setEnv("GOOS", this.String()) +} + +type oses []os + +func (this oses) String() string { + return strings.Join(this.Strings(), ",") +} + +func (this oses) Strings() []string { + strs := make([]string, len(this)) + for i, v := range this { + strs[i] = v.String() + } + return strs +} + +func (this *oses) Set(plain string) error { + parts := strings.Split(plain, ",") + buf := make(oses, len(parts)) + for i, part := range parts { + part = strings.TrimSpace(part) + if err := buf[i].Set(part); err != nil { + return err + } + } + *this = buf + return nil +} + +var ( + osToString = map[os]string{ + osLinux: "linux", + osWindows: "windows", + } + stringToOs = func(in map[os]string) map[string]os { + result := make(map[string]os, len(in)) + for k, v := range in { + result[v] = k + } + return result + }(osToString) + allOsVariants = func(in map[os]string) oses { + result := make(oses, len(in)) + var i int + for k := range in { + result[i] = k + i++ + } + return result + }(osToString) +) diff --git a/cmd/build/platform.go b/cmd/build/platform.go new file mode 100644 index 0000000..0660944 --- /dev/null +++ b/cmd/build/platform.go @@ -0,0 +1,117 @@ +package main + +import ( + "fmt" + "iter" +) + +type platform struct { + os os + arch arch + edition edition + testing bool +} + +func (this platform) String() string { + var result string + if v := this.os; v != 0 { + result = v.String() + } + if v := this.arch; v != 0 { + if result != "" { + result += "/" + } + result += v.String() + } + if v := this.edition; v != 0 { + if result != "" { + result += "/" + } + result += v.String() + } + if this.testing { + result += "(testing)" + } + return result +} + +func (this platform) ociString() string { + return this.os.String() + "/" + this.arch.ociString() +} + +func (this platform) from() (name string, _ error) { + b := this.arch.details().os[this.os] + + switch this.edition { + case editionExtended: + name = b.fromImageExtended + default: + name = b.fromImage + } + + if name == "" { + return "", fmt.Errorf("%v is not supported for image creation", this) + } + + return name, nil +} + +func (this platform) setToEnv(assumedGoos os, assumedGoarch arch, env interface{ setEnv(key, val string) }) { + this.arch.setToEnv(this.os, assumedGoos, assumedGoarch, env) + this.edition.setToEnv(env) +} + +func (this platform) isBinarySupported(assumedOs os, assumedArch arch) bool { + return this.edition.isBinarySupported(this.os, this.arch, assumedOs, assumedArch) +} + +func (this platform) assertBinarySupported(assumedOs os, assumedArch arch) error { + if this.isBinarySupported(assumedOs, assumedArch) { + return nil + } + return fmt.Errorf("combination %v is not supported for binaries", this) +} + +func (this platform) isImageSupported() bool { + b := this.arch.details().os[this.os] + + switch this.edition { + case editionExtended: + return b.fromImageExtended != "" + default: + return b.fromImage != "" + } +} + +func (this platform) toLdFlags(o os) string { + return this.edition.toLdFlags(o) +} + +func (this platform) filenamePrefix(mainPrefix string) string { + result := mainPrefix + + "-" + this.os.String() + + "-" + this.arch.String() + + "-" + this.edition.String() + if this.testing { + result += "-testing" + } + return result +} + +func allBinaryPlatforms(forTesting bool, assumedOs os, assumedArch arch) iter.Seq[*platform] { + return func(yield func(*platform) bool) { + for _, o := range allOsVariants { + for _, a := range allArchVariants { + if a.isOsSupported(o) { + for _, e := range allEditionVariants { + if e.isBinarySupported(o, a, assumedOs, assumedArch) { + if !yield(&platform{o, a, e, forTesting}) { + return + } + } + } + } + } + } + } +} diff --git a/cmd/build/repo-actions.go b/cmd/build/repo-actions.go new file mode 100644 index 0000000..fa5a6d2 --- /dev/null +++ b/cmd/build/repo-actions.go @@ -0,0 +1,96 @@ +package main + +import ( + "context" + "fmt" + "iter" + + "github.com/alecthomas/kingpin" + "github.com/google/go-github/v65/github" +) + +func newRepoActions(r *repo) *repoActions { + return &repoActions{ + repo: r, + + workflowFilenameCi: "ci.yml", + } +} + +type repoActions struct { + *repo + + workflowFilenameCi string +} + +func (this *repoActions) init(_ context.Context, app *kingpin.Application) { + app.Flag("workflow-filename-ci", ""). + Default(this.workflowFilenameCi). + StringVar(&this.workflowFilenameCi) +} + +func (this *repoActions) ciWorkflow(ctx context.Context) (*repoWorkflow, error) { + return this.workflowByFilename(ctx, this.workflowFilenameCi) +} + +func (this *repoActions) workflowByFilename(ctx context.Context, fn string) (*repoWorkflow, error) { + v, _, err := this.client().Actions.GetWorkflowByFileName(ctx, this.owner.String(), this.name.String(), fn) + if err != nil { + return nil, fmt.Errorf("cannot retrieve workflow %s from %v: %w", fn, this.base, err) + } + return &repoWorkflow{ + v, + this, + }, nil +} + +type repoWorkflow struct { + *github.Workflow + + parent *repoActions +} + +func (this *repoWorkflow) String() string { + return fmt.Sprintf("%s(%d)@%v", *this.Name, *this.ID, this.parent.repo) +} + +func (this *repoWorkflow) runs(ctx context.Context) iter.Seq2[*repoWorkflowRun, error] { + return func(yield func(*repoWorkflowRun, error) bool) { + var opts github.ListWorkflowRunsOptions + opts.PerPage = 100 + + for { + candidates, rsp, err := this.parent.client().Actions.ListWorkflowRunsByID(ctx, this.parent.owner.String(), this.parent.name.String(), *this.ID, &opts) + if err != nil { + yield(nil, fmt.Errorf("cannot retrieve workflow runs of %v (page: %d): %w", this, opts.Page, err)) + return + } + for _, v := range candidates.WorkflowRuns { + if !yield(&repoWorkflowRun{v, this}, nil) { + return + } + } + if rsp.NextPage == 0 { + return + } + opts.Page = rsp.NextPage + } + } +} + +type repoWorkflowRun struct { + *github.WorkflowRun + parent *repoWorkflow +} + +func (this *repoWorkflowRun) rerun(ctx context.Context) error { + _, err := this.parent.parent.client().Actions.RerunWorkflowByID(ctx, this.parent.parent.owner.String(), this.parent.parent.name.String(), *this.ID) + if err != nil { + return fmt.Errorf("cannot rerun workflow run %v: %w", this, err) + } + return nil +} + +func (this repoWorkflowRun) String() string { + return fmt.Sprintf("%d@%v", *this.ID, this.parent) +} diff --git a/cmd/build/repo-packages.go b/cmd/build/repo-packages.go new file mode 100644 index 0000000..0cb201d --- /dev/null +++ b/cmd/build/repo-packages.go @@ -0,0 +1,123 @@ +package main + +import ( + "context" + "fmt" + "iter" + "slices" + + "github.com/alecthomas/kingpin" + "github.com/echocat/slf4g" + "github.com/google/go-github/v65/github" +) + +func newRepoPackages(r *repo) *repoPackages { + return &repoPackages{ + repo: r, + } +} + +type repoPackages struct { + *repo +} + +func (this *repoPackages) init(_ context.Context, _ *kingpin.Application) {} + +func (this *repoPackages) deleteVersionsWithTags(ctx context.Context, tags ...string) error { + del := func(sub string) error { + for candidate, err := range this.versionsWithAtLeastOneTag(ctx, sub, tags) { + if err != nil { + return err + } + + l := log.With("packageVersion", *candidate.ID). + With("packageVersionUrl", *candidate.HTMLURL) + if err := candidate.delete(ctx); err != nil { + l.WithError(err).Warn() + } else { + l.Info("successfully deleted") + } + } + return nil + } + + if err := del(""); err != nil { + return err + } + + return nil +} + +func (this *repoPackages) versions(ctx context.Context, sub string) iter.Seq2[*repoPackageVersion, error] { + return func(yield func(*repoPackageVersion, error) bool) { + var opts github.PackageListOptions + opts.PerPage = 100 + + for { + candidates, rsp, err := this.client().Organizations.PackageGetAllVersions( + ctx, + this.owner.String(), + "container", + this.SubName(sub), + &opts, + ) + if err != nil { + yield(nil, fmt.Errorf("cannot retrieve package versions information for %s (page: %d): %w", this.SubString(sub), opts.Page, err)) + return + } + for _, v := range candidates { + if !yield(&repoPackageVersion{v, sub, this}, nil) { + return + } + } + if rsp.NextPage == 0 { + return + } + opts.Page = rsp.NextPage + } + } +} + +func (this *repoPackages) versionsWithAtLeastOneTag(ctx context.Context, sub string, tags []string) iter.Seq2[*repoPackageVersion, error] { + return func(yield func(*repoPackageVersion, error) bool) { + for candidate, err := range this.versions(ctx, sub) { + if err != nil { + yield(nil, err) + return + } + if candidate.Metadata != nil && candidate.Metadata.Container != nil { + if slices.ContainsFunc(candidate.Metadata.Container.Tags, func(s string) bool { + return slices.Contains(tags, s) + }) { + if !yield(candidate, nil) { + return + } + } + } + } + } +} + +type repoPackageVersion struct { + *github.PackageVersion + sub string + parent *repoPackages +} + +func (this *repoPackageVersion) delete(ctx context.Context) error { + if _, err := this.parent.client().Organizations.PackageDeleteVersion( + ctx, + this.parent.owner.String(), + "container", + this.parent.base.repo.SubName(this.sub), + *this.ID, + ); err != nil { + return fmt.Errorf("cannot delete package version %v: %w", this, err) + } + + return nil +} + +func (this repoPackageVersion) String() string { + return fmt.Sprintf("%s(%d)@%s", *this.Name, *this.ID, this.parent.SubString(this.sub)) +} diff --git a/cmd/build/repo-prs.go b/cmd/build/repo-prs.go new file mode 100644 index 0000000..1dbfb58 --- /dev/null +++ b/cmd/build/repo-prs.go @@ -0,0 +1,198 @@ +package main + +import ( + "context" + "fmt" + "slices" + "strings" + "time" + + "github.com/alecthomas/kingpin" + "github.com/echocat/slf4g" + "github.com/google/go-github/v65/github" +) + +func newRepoPrs(r *repo) *repoPrs { + return &repoPrs{ + repo: r, + + testPublishLabel: "test_publish", + } +} + +type repoPrs struct { + *repo + + testPublishLabel string +} + +func (this *repoPrs) Validate() error { return nil } + +func (this *repoPrs) init(ctx context.Context, app *kingpin.Application) { + app.Flag("label-test-publish", ""). + Default(this.testPublishLabel). + StringVar(&this.testPublishLabel) + + var eventAction string + var label string + + cmdIu := app.Command("inspect-pr-action", "") + cmdIu.Arg("eventAction", ""). + Required(). + StringVar(&eventAction) + cmdIu.Arg("label", ""). + StringVar(&label) + cmdIu.Action(func(*kingpin.ParseContext) error { + return this.inspectAction(ctx, eventAction, label) + }) +} + +func (this *repoPrs) inspectAction(ctx context.Context, eventAction string, label string) error { + prNumber := this.pr() + if prNumber == 0 { + return fmt.Errorf("the environment does not contain a pr reference") + } + + pr, err := this.byId(ctx, prNumber) + if err != nil { + return err + } + + if (eventAction == "labeled" || eventAction == "unlabeled") && label == "" { + return fmt.Errorf("for labeled and unlabeled actions the label argument is required") + } + + if eventAction == "labeled" && label == this.testPublishLabel && pr.isOpen() { + log.With("pr", pr). + With("label", this.testPublishLabel). + Info("PR received label for being allowed to publish; rerun the latest workflow to enable them now...") + + if err := pr.rerunLatestCiWorkflow(ctx); err != nil { + return err + } + } + + if (eventAction == "unlabeled" && label == this.testPublishLabel && pr.isOpen()) || + eventAction == "closed" { + + log.With("pr", pr). + With("label", this.testPublishLabel). + Info("PR was unlabeled or closed; therefore delete all images that might be related to this PR...") + + if err := pr.deleteRelatedArtifacts(ctx); err != nil { + return err + } + } + + return nil +} + +func (this *repoPrs) byId(ctx context.Context, number uint) (*repoPr, error) { + v, _, err := this.client().PullRequests.Get(ctx, this.owner.String(), this.name.String(), int(number)) + if err != nil { + return nil, fmt.Errorf("cannot retrieve pull request %d from %v: %w", number, this.base, err) + } + return &repoPr{ + v, + this, + }, nil +} + +type repoPr struct { + *github.PullRequest + + parent *repoPrs +} + +func (this *repoPr) String() string { + return fmt.Sprintf("%d@%v", *this.ID, this.parent.base.repo) +} + +func (this *repoPr) hasLabel(label string) bool { + return slices.ContainsFunc(this.Labels, func(candidate *github.Label) bool { + if candidate == nil { + return false + } + return candidate.Name != nil && *candidate.Name == label + }) +} + +func (this *repoPr) isOpen() bool { + return this.State != nil && strings.EqualFold(*this.State, "open") +} + +func (this *repoPr) rerunLatestCiWorkflow(ctx context.Context) error { + return this.rerunLatestWorkflow(ctx, this.parent.actions.ciWorkflow) +} + +func (this *repoPr) rerunLatestWorkflow(ctx context.Context, workflowLoader func(context.Context) (*repoWorkflow, error)) error { + l := log.With("pr", this.GetID()) + + start := time.Now() + for ctx.Err() == nil { + wfr, err := this.latestWorkflowRun(ctx, workflowLoader) + if err != nil { + return err + } + lw := l.With("workflowRun", wfr) + if wfr.Status != nil && strings.EqualFold(*wfr.Status, "completed") { + if err := wfr.rerun(ctx); err != nil { + return err + } + lw.With("workflowRunUrl", *wfr.HTMLURL). + With("prUrl", this.GetHTMLURL()). + Info("rerun of workflow run was successfully triggered") + return nil + } + lw.With("duration", time.Since(start).Truncate(time.Second)). + Info("latest workflow run is still running - continue waiting...") + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(this.parent.base.waitTimeout): + } + } + + return ctx.Err() +} + +func (this *repoPr) latestWorkflowRun(ctx context.Context, workflowLoader func(context.Context) (*repoWorkflow, error)) (*repoWorkflowRun, error) { + wf, err := workflowLoader(ctx) + if err != nil { + return nil, fmt.Errorf("cannot get workflow of %v: %w", this, err) + } + for candidate, err := range wf.runs(ctx) { + if err != nil { + return nil, fmt.Errorf("cannot retrieve workflow runs for pr %v: %w", this, err) + } + if slices.ContainsFunc(candidate.PullRequests, func(cpr *github.PullRequest) bool { + return cpr != nil && cpr.ID != nil && *cpr.ID == *this.ID + }) { + return candidate, nil + } + } + return nil, nil +} + +func (this *repoPr) deleteRelatedArtifacts(ctx context.Context) error { + fail := func(err error) error { + return fmt.Errorf("cannot delete artifacts for %v: %w", this, err) + } + + do := func(tag string) error { + return this.parent.actions.packages.deleteVersionsWithTags(ctx, tag) + } + + mainTag := fmt.Sprintf("pr-%d", this.GetID()) + + if err := do(mainTag); err != nil { + return fail(err) + } + for _, ed := range allEditionVariants { + if err := do(ed.String() + "-" + mainTag); err != nil { + return fail(err) + } + } + + return nil +} diff --git a/cmd/build/repo-releases.go b/cmd/build/repo-releases.go new file mode 100644 index 0000000..9e27205 --- /dev/null +++ b/cmd/build/repo-releases.go @@ -0,0 +1,151 @@ +package main + +import ( + "context" + "fmt" + "iter" + "net/http" + gos "os" + "strings" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/alecthomas/kingpin" + log "github.com/echocat/slf4g" + "github.com/google/go-github/v65/github" + + "github.com/engity-com/bifroest/pkg/common" +) + +func newRepoReleases(r *repo) *repoReleases { + return &repoReleases{ + repo: r, + } +} + +func (this *repoReleases) init(_ context.Context, _ *kingpin.Application) {} + +type repoReleases struct { + *repo +} + +func (this *repoReleases) findCurrent(ctx context.Context) (*repoRelease, error) { + ref, err := this.ref(ctx) + if err != nil { + return nil, err + } + v, resp, err := this.client().Repositories.GetReleaseByTag(ctx, this.owner.String(), this.name.String(), ref) + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("cannot retrieve current release: %w", err) + } + return &repoRelease{v, this}, nil +} + +func (this *repoReleases) all(ctx context.Context) iter.Seq2[*repoRelease, error] { + return func(yield func(*repoRelease, error) bool) { + var opts github.ListOptions + opts.PerPage = 100 + + for { + candidates, rsp, err := this.client().Repositories.ListReleases(ctx, this.owner.String(), this.name.String(), &opts) + if err != nil { + yield(nil, fmt.Errorf("cannot retrieve releases (page: %d): %w", opts.Page, err)) + return + } + for _, v := range candidates { + if !yield(&repoRelease{v, this}, nil) { + return + } + } + if rsp.NextPage == 0 { + return + } + opts.Page = rsp.NextPage + } + } +} + +func (this *repoReleases) allSemver(ctx context.Context) iter.Seq2[*semver.Version, error] { + return func(yield func(*semver.Version, error) bool) { + for r, err := range this.all(ctx) { + if err != nil && !yield(nil, err) { + return + } + + if r.TagName == nil { + continue + } + if !strings.HasPrefix(*r.TagName, "v") { + continue + } + + rsmv, err := semver.NewVersion((*r.TagName)[1:]) + if err != nil { + if yield(nil, err) { + continue + } else { + return + } + } else if !yield(rsmv, nil) { + return + } + } + } +} + +type repoRelease struct { + *github.RepositoryRelease + + parent *repoReleases +} + +func (this *repoRelease) String() string { + return fmt.Sprintf("%s(%d)@%v", *this.Name, *this.ID, this.parent.repo) +} + +func (this *repoRelease) uploadAsset(ctx context.Context, name, mediaType, label, fn string) (*repoReleaseAsset, error) { + fail := func(err error) (*repoReleaseAsset, error) { + return nil, fmt.Errorf("cannot upload asset %q: %w", name, err) + } + + f, err := gos.Open(fn) + if err != nil { + return fail(err) + } + defer common.IgnoreCloseError(f) + + l := log.With("release", this). + With("name", name). + With("mediaType", mediaType). + With("label", label) + + start := time.Now() + l.Debug("uploading asset...") + + asset, _, err := this.parent.client().Repositories.UploadReleaseAsset(ctx, this.parent.owner.String(), this.parent.name.String(), this.GetID(), &github.UploadOptions{ + Name: name, + Label: label, + MediaType: mediaType, + }, f) + if err != nil { + return fail(err) + } + + ll := l.With("duration", time.Since(start).Truncate(time.Millisecond)) + if l.IsDebugEnabled() { + ll.Info("uploading asset... DONE!") + } else { + ll.Info("asset uploaded") + } + + return &repoReleaseAsset{asset, this.parent}, nil +} + +type repoReleaseAsset struct { + *github.ReleaseAsset + + parent *repoReleases +} diff --git a/cmd/build/repo.go b/cmd/build/repo.go new file mode 100644 index 0000000..92b44fc --- /dev/null +++ b/cmd/build/repo.go @@ -0,0 +1,192 @@ +package main + +import ( + "context" + "fmt" + gos "os" + "regexp" + "runtime" + "strings" + "sync/atomic" + + "github.com/alecthomas/kingpin" + "github.com/google/go-github/v65/github" +) + +const ( + fallbackOwner = "engity-com" + fallbackRepo = "bifroest" +) + +var ( + defaultOwner, defaultRepo = func() (owner, repo string) { + v, ok := gos.LookupEnv("GITHUB_REPOSITORY") + if !ok { + return fallbackOwner, fallbackRepo + } + parts := strings.Split(v, "/") + if len(parts) != 2 { + return fallbackOwner, fallbackRepo + } + return parts[0], parts[1] + }() +) + +func newRepo(b *base) *repo { + result := &repo{ + base: b, + } + result.packages = newRepoPackages(result) + result.prs = newRepoPrs(result) + result.actions = newRepoActions(result) + result.releases = newRepoReleases(result) + return result +} + +func (this *repo) init(ctx context.Context, app *kingpin.Application) { + app.Flag("githubToken", ""). + Envar("GITHUB_TOKEN"). + Required(). + PlaceHolder("<token>"). + StringVar(&this.githubToken) + app.Flag("owner", ""). + Default(defaultOwner). + PlaceHolder("<owner>"). + SetValue(&this.owner) + app.Flag("repo", ""). + Default(defaultRepo). + PlaceHolder("<repo>"). + SetValue(&this.name) + this.packages.init(ctx, app) + this.prs.init(ctx, app) + this.actions.init(ctx, app) + this.releases.init(ctx, app) +} + +type repo struct { + *base + + githubToken string + owner owner + name repoName + + packages *repoPackages + prs *repoPrs + actions *repoActions + releases *repoReleases + + clientP atomic.Pointer[github.Client] + metaP atomic.Pointer[github.Repository] +} + +func (this *repo) String() string { + return fmt.Sprintf("%s/%s", this.owner, this.name) +} + +func (this *repo) fullName() string { + return fmt.Sprintf("github.com/%s/%s", this.owner, this.name) +} + +func (this *repo) fullImageName() string { + return fmt.Sprintf("ghcr.io/%s/%s", this.owner, this.name) +} + +func (this *repo) SubName(sub string) string { + if sub == "" { + return this.name.String() + } + return fmt.Sprintf("%v%%2F%s", this.name, sub) +} + +func (this *repo) SubString(sub string) string { + if sub == "" { + return this.String() + } + return fmt.Sprintf("%v/%s", this, sub) +} + +func (this *repo) client() *github.Client { + for { + v := this.clientP.Load() + if v != nil { + return v + } + v = github.NewClient(nil). + WithAuthToken(this.githubToken) + if this.clientP.CompareAndSwap(nil, v) { + return v + } + runtime.Gosched() + } +} + +func (this *repo) meta(ctx context.Context) (*github.Repository, error) { + for { + v := this.metaP.Load() + if v != nil { + return v, nil + } + v, _, err := this.client().Repositories.Get(ctx, this.owner.String(), this.name.String()) + if err != nil { + return nil, err + } + if this.metaP.CompareAndSwap(nil, v) { + return v, nil + } + runtime.Gosched() + } +} + +type owner string + +func (this owner) String() string { + return string(this) +} + +var ownerRegex = regexp.MustCompile("^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$") + +func (this *owner) Set(v string) error { + buf := owner(v) + if err := buf.Validate(); err != nil { + return err + } + *this = buf + return nil +} + +func (this owner) Validate() error { + if this == "" { + return fmt.Errorf("no owner provided") + } + if !ownerRegex.MatchString(string(this)) { + return fmt.Errorf("illegal owner: %s", this) + } + return nil +} + +type repoName string + +func (this repoName) String() string { + return string(this) +} + +var repoNameRegex = regexp.MustCompile("^[a-zA-Z0-9-_.]+$") + +func (this *repoName) Set(v string) error { + buf := repoName(v) + if err := buf.Validate(); err != nil { + return err + } + *this = repoName(v) + return nil +} + +func (this repoName) Validate() error { + if this == "" { + return fmt.Errorf("no repo name provided") + } + if !repoNameRegex.MatchString(string(this)) { + return fmt.Errorf("illegal repo name: %s", this) + } + return nil +} diff --git a/cmd/build/version.go b/cmd/build/version.go new file mode 100644 index 0000000..87d619f --- /dev/null +++ b/cmd/build/version.go @@ -0,0 +1,153 @@ +package main + +import ( + "fmt" + "iter" + "regexp" + "strconv" + "strings" + + "github.com/Masterminds/semver/v3" +) + +var ( + versionPattern = regexp.MustCompile(`^\w[\w.-]{0,127}$`) + + versionNormalizePattern = regexp.MustCompile(`[^\w]+`) +) + +type version struct { + semver *semver.Version + raw string + + latestMajor bool + latestMinor bool + latestPatch bool +} + +func (this *version) Set(plain string) error { + var buf version + + if plain != "" { + if !versionPattern.MatchString(plain) { + return fmt.Errorf("invalid version: %s", plain) + } + + buf.raw = plain + if strings.HasPrefix(plain, "v") { + v, err := semver.NewVersion(plain[1:]) + if err == nil { + buf.semver = v + } + } + } + + *this = buf + return nil +} + +func (this version) String() string { + return this.raw +} + +func (this version) tags(prefix string, rootTag string) iter.Seq[string] { + return func(yield func(string) bool) { + smv := this.semver + if smv == nil { + yield(prefix + this.raw) + return + } + + f := func(root string, vs ...uint64) string { + result := prefix + if len(vs) == 0 { + result = root + } + for i, v := range vs { + if i > 0 { + result += "." + } + result += strconv.FormatUint(v, 10) + } + if v := smv.Prerelease(); v != "" { + result += "-" + v + } + if v := smv.Metadata(); v != "" { + result += "+" + v + } + return result + } + + if !yield(prefix + smv.String()) { + return + } + + if this.latestPatch { + if !yield(f("", smv.Major(), smv.Minor())) { + return + } + } else { + return + } + + if this.latestMinor { + if !yield(f("", smv.Major())) { + return + } + } else { + return + } + + if this.latestMajor && rootTag != "" { + if !yield(f(rootTag)) { + return + } + } else { + return + } + } +} + +func (this *version) evaluateLatest(i iter.Seq2[*semver.Version, error]) error { + fail := func(err error) error { + return fmt.Errorf("cannot evaluate version's %v latest states: %w", this, err) + } + + tv := this.semver + if tv == nil { + return nil + } + + major, minor, patch := true, true, true + + for ov, err := range i { + if err != nil { + return fail(err) + } + + if ov.Major() > tv.Major() { + major = false + } else if ov.Major() == tv.Major() { + if ov.Minor() > tv.Minor() { + major = false + minor = false + } else if ov.Minor() == tv.Minor() { + if ov.Patch() > tv.Patch() { + major = false + minor = false + patch = false + } + } + } + + if !major && !minor && !patch { + break + } + } + + this.latestMajor = major + this.latestMinor = minor + this.latestPatch = patch + + return nil +} diff --git a/cmd/build/version_test.go b/cmd/build/version_test.go new file mode 100644 index 0000000..53d2295 --- /dev/null +++ b/cmd/build/version_test.go @@ -0,0 +1,199 @@ +package main + +import ( + "iter" + "slices" + "strings" + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVersion_evaluateLatest(t *testing.T) { + var instance version + + require.NoError(t, instance.Set("v2.3.4")) + + cases := []struct { + input []string + + expectedMajor bool + expectedMinor bool + expectedPatch bool + }{{ + input: a[string](), + expectedMajor: true, + expectedMinor: true, + expectedPatch: true, + }, { + input: a[string]("2.3.4"), + expectedMajor: true, + expectedMinor: true, + expectedPatch: true, + }, { + input: a[string]("2.3.4", "1.2.3"), + expectedMajor: true, + expectedMinor: true, + expectedPatch: true, + }, { + input: a[string]("2.3.4", "1.2.3", "2.3.3"), + expectedMajor: true, + expectedMinor: true, + expectedPatch: true, + }, { + input: a[string]("2.3.4", "1.2.3", "3.0.0"), + expectedMajor: false, + expectedMinor: true, + expectedPatch: true, + }, { + input: a[string]("2.3.4", "1.2.3", "3.0.0", "2.3.3"), + expectedMajor: false, + expectedMinor: true, + expectedPatch: true, + }, { + input: a[string]("2.3.4", "1.2.3", "3.1.0", "2.3.3"), + expectedMajor: false, + expectedMinor: true, + expectedPatch: true, + }, { + input: a[string]("2.3.4", "1.2.3", "2.3.5"), + expectedMajor: false, + expectedMinor: false, + expectedPatch: false, + }, { + input: a[string]("2.3.4", "1.2.3", "2.4.0"), + expectedMajor: false, + expectedMinor: false, + expectedPatch: true, + }} + + for _, c := range cases { + t.Run(strings.Join(c.input, ","), func(t *testing.T) { + given := instance.cleanClone() + + actualErr := given.evaluateLatest(allSemver(c.input...)) + require.NoError(t, actualErr) + assert.Equal(t, c.expectedMajor, given.latestMajor) + assert.Equal(t, c.expectedMinor, given.latestMinor) + assert.Equal(t, c.expectedPatch, given.latestPatch) + }) + } +} + +func TestVersion_tags_semver(t *testing.T) { + var instance version + + require.NoError(t, instance.Set("v2.3.4")) + + cases := []struct { + major bool + minor bool + patch bool + root string + + outputs []string + }{{ + major: false, + minor: false, + patch: false, + root: "ll", + outputs: a("x2.3.4"), + }, { + major: false, + minor: false, + patch: true, + root: "ll", + outputs: a("x2.3.4", "x2.3"), + }, { + major: false, + minor: true, + patch: true, + root: "ll", + outputs: a("x2.3.4", "x2.3", "x2"), + }, { + major: true, + minor: true, + patch: true, + root: "ll", + outputs: a("x2.3.4", "x2.3", "x2", "ll"), + }, { + major: true, + minor: true, + patch: false, + root: "ll", + outputs: a("x2.3.4"), + }, { + major: true, + minor: false, + patch: true, + root: "ll", + outputs: a("x2.3.4", "x2.3"), + }} + + for _, c := range cases { + t.Run(strings.Join(c.outputs, ","), func(t *testing.T) { + given := instance.cleanClone() + given.latestMajor = c.major + given.latestMinor = c.minor + given.latestPatch = c.patch + + actual := slices.Collect(given.tags("x", c.root)) + assert.Equal(t, c.outputs, actual) + }) + } +} + +func TestVersion_tags_other(t *testing.T) { + cases := []struct { + input string + prefix string + root string + outputs []string + }{{ + input: "x123x", + prefix: "p", + root: "r", + outputs: a("px123x"), + }, { + input: "v1.2.3", + prefix: "p", + root: "r", + outputs: a("p1.2.3"), + }} + + for _, c := range cases { + t.Run(c.input+"-"+c.prefix+"-"+c.root, func(t *testing.T) { + var instance version + require.NoError(t, instance.Set(c.input)) + + actual := slices.Collect(instance.tags(c.prefix, c.root)) + assert.Equal(t, c.outputs, actual) + }) + } +} + +func a[T any](in ...T) []T { + return in +} + +func allSemver(in ...string) iter.Seq2[*semver.Version, error] { + return func(yield func(*semver.Version, error) bool) { + for _, plain := range in { + if !yield(semver.MustParse(plain), nil) { + return + } + } + } +} + +func (this version) cleanClone() version { + return version{ + this.semver, + this.raw, + false, + false, + false, + } +} diff --git a/docs/.theme/marcos/main.py b/docs/.theme/marcos/main.py index 136734e..6cc1247 100644 --- a/docs/.theme/marcos/main.py +++ b/docs/.theme/marcos/main.py @@ -1,19 +1,210 @@ import json -import os -import os.path -from typing import Sequence - +import os as pos +import os.path as path +from collections import OrderedDict +from enum import Enum +from pathlib import PurePath +from typing import Sequence, List + +from mkdocs.structure.files import File +from mkdocs_macros.context import Files from mkdocs_macros.plugin import MacrosPlugin repo = "engity-com/bifroest" repo_http_url = "https://github.com/" + repo repo_raw_url = "https://raw.githubusercontent.com/" + repo -raw_version = os.getenv('VERSION') +raw_version = pos.getenv('VERSION') version = raw_version if raw_version is not None else "development" branch = raw_version if raw_version is not None else "main" release = raw_version if raw_version is not None else "latest" +class Os(str, Enum): + linux = 'linux' + windows = 'windows' + + +class Arch(str, Enum): + i386 = '386' + amd64 = 'amd64' + armv6 = 'armv6' + armv7 = 'armv7' + arm64 = 'arm64' + mips64le = 'mips64le' + riscv64 = 'riscv64' + + +class EditionKind(str, Enum): + generic = 'generic' + extended = 'extended' + + +class Edition: + os: Os + arch: Arch + kind: EditionKind + binary_supported: bool + image_supported: bool + + def __init__( + self, + o: Os, + arch: Arch, + kind: EditionKind, + binary_supported: bool = False, + image_supported: bool = False + ): + self.os = o + self.arch = arch + self.kind = kind + self.binary_supported = binary_supported + self.image_supported = image_supported + + if not binary_supported and image_supported: + raise Exception(f"image can't be supported if binary isn't") + + +def editions_of( + o: Os, + arch: Arch, + generic_binary_supported: bool = False, + generic_image_supported: bool = False, + extended_binary_supported: bool = False, + extended_image_supported: bool = False, +) -> List[Edition]: + if not generic_binary_supported and extended_binary_supported: + raise Exception(f"extended can't be supported if generic isn't") + + if not generic_binary_supported: + return [] + + generic = Edition(o, arch, EditionKind.generic, generic_binary_supported, generic_image_supported) + + if not extended_binary_supported: + return [generic] + + return [ + generic, + Edition(o, arch, EditionKind.extended, extended_binary_supported, extended_image_supported), + ] + + +class SupportMatrix: + entries: OrderedDict[Os, OrderedDict[Arch, OrderedDict[EditionKind, Edition]]] + + def __init__(self, *edss: List[Edition]): + self.entries: OrderedDict[Os, OrderedDict[Arch, OrderedDict[EditionKind, Edition]]] = OrderedDict({}) + + for eds in edss: + for ed in eds: + if not self.entries.__contains__(ed.os): + self.entries[ed.os] = OrderedDict[Arch, OrderedDict[EditionKind, Edition]]({}) + by_os = self.entries[ed.os] + + if not by_os.__contains__(ed.arch): + by_os[ed.arch] = OrderedDict[EditionKind, Edition]({}) + by_arch = by_os[ed.arch] + + by_arch[ed.kind] = ed + + def lookup( + self, + os: Os | str, + arch: Arch | str, + kind: EditionKind | str + ) -> Edition | None: + + if type(os) is str: + os = Os[os] + + if type(arch) is str: + arch = Arch[arch] + + if type(kind) is str: + kind = EditionKind[kind] + + if not self.entries.__contains__(os): + return None + + if not self.entries[os].__contains__(arch): + return None + + if not self.entries[os][arch].__contains__(kind): + return None + + return self.entries[os][arch][kind] + + def is_binary_supported( + self, + os: Os | str, + arch: Arch | str, + kind: EditionKind | str + ) -> bool: + + ed = self.lookup(os, arch, kind) + + return False if ed.binary_supported is None else ed.binary_supported + + def is_image_supported( + self, + os: Os | str, + arch: Arch | str, + kind: EditionKind | str + ) -> bool: + + ed = self.lookup(os, arch, kind) + + return False if ed.image_supported is None else ed.image_supported + + +support_matrix = SupportMatrix( + editions_of( + Os.linux, Arch.i386, + True, True, + True, False + ), + editions_of( + Os.linux, Arch.amd64, + True, True, + True, True + ), + editions_of( + Os.linux, Arch.armv6, + True, True, + True, False + ), + editions_of( + Os.linux, Arch.armv7, + True, True, + True, True + ), + editions_of( + Os.linux, Arch.arm64, + True, True, + True, True + ), + editions_of( + Os.linux, Arch.mips64le, + True, True, + True, False + ), + editions_of( + Os.linux, Arch.riscv64, + True, True, + True, False + ), + + editions_of( + Os.windows, Arch.amd64, + True, True, + ), + editions_of( + Os.windows, Arch.arm64, + True, False, + ) +) + + class TypeRefT: @property def title(self) -> str: @@ -260,7 +451,7 @@ def asset_url(file: str, raw: bool = False) -> str: @env.macro def asset_link(file: str, title: str | None = None, raw: bool = False) -> str: url = asset_url(file, raw) - title = title if title is not None else os.path.basename(file) + title = title if title is not None else path.basename(file) return f"<a href={url}>{title}</a>" @@ -277,15 +468,119 @@ def release_asset_url(asset: str, target: str = release) -> str: return f"{repo_http_url}/releases/download/{target}/{asset}" @env.macro - def rel_file_path(path: str, start: str) -> str: - return os.path.relpath(path, os.path.dirname(start)) + def rel_file_path(in_path: str, start: str) -> str: + return path.relpath(in_path, path.dirname(start)) + + @env.macro + def compatibility( + supported: bool | None = False, + label: str | None = None, + os: Os | str | None = None + ) -> str: + title = None + if label is not None: + title = f"<code>{label}</code>" + if os is not None: + if type(os) is str: + os = Os[os] + + title = f"<code>{os.name}</code>{f"/{title}" if title is not None else ""}" + + if supported is None: + return f":octicons-circle-24:{{. data-supported=none title='{f"{title} is not supported" if title is not None else "Not supported"}'}}" + elif supported: + return f":octicons-check-circle-24:{{. data-supported=true title='{f"{title} is supported" if title is not None else "Supported"}'}}" + else: + return f":octicons-x-circle-24:{{. data-supported=false title='{f"{title} is not supported" if title is not None else "Not supported"}'}}" + + @env.macro + def compatibility_editions( + generic: bool | None = False, + extended: bool | None = False, + os: Os | str | None = None + ) -> str: + if os is None: + return f"{compatibility(generic, "generic")}/{compatibility(extended, "extended")}" + else: + if type(os) is str: + os = Os[os] + + files: Files = env.variables.files + file: File = files.get_file_from_path("setup/distribution.md") + dst = PurePath(path.relpath(file.src_path, path.dirname(env.page.file.src_path))) + return (f"[{compatibility(generic, "generic", os)}]({dst.as_posix()}#{os.name}-generic)/" + f"[{compatibility(extended, "extended", os)}]({dst.as_posix()}#{os.name}-extended)") + + @env.macro + def is_binary_supported(o: Os | str, arch: Arch | str, kind: EditionKind | str) -> bool: + return support_matrix.is_binary_supported(o, arch, kind) @env.macro - def compatibility(supported: bool = False) -> str: - if supported: - return ":octicons-check-circle-24:{. data-supported=true title='Supported'} `*`" + def is_image_supported(o: Os | str, arch: Arch | str, kind: EditionKind | str) -> bool: + return support_matrix.is_image_supported(o, arch, kind) - return ":octicons-x-circle-24:{. data-supported=false title='Not supported'}" + @env.macro + def compatibility_matrix( + os: Os | None = None, + ) -> str: + result = '<table markdown="span" data-kind="compatibility_matrix"><thead markdown="span">' + result += '<tr markdown="span"><th rowspan="2">Architecture</th>' + if os is not None: + result += f'<th colspan="2">{dist(os)}</th>' + else: + for osv in Os: + result += f'<th colspan="2" markdown="span">{dist(osv)}</th>' + result += "</tr>" + + if os is not None: + result += '<th>Binary</th><th>Image</th>' + else: + for _ in Os: + result += '<th>Binary</th><th>Image</th>' + result += '</tr>' + + result += '</thead><tbody markdown="span">' + + for arch in Arch: + result += f'<tr markdown="span"><td markdown="span">`{arch.name}`</td>' + + if os is not None: + generic = support_matrix.lookup(os, arch, EditionKind.generic) + extended = support_matrix.lookup(os, arch, EditionKind.extended) + result += f'<td markdown="span">{compatibility_editions(True if generic and generic.binary_supported else None, True if extended and extended.binary_supported else None, os)}</td>' + result += f'<td markdown="span">{compatibility_editions(True if generic and generic.image_supported else None, True if extended and extended.image_supported else None, os)}</td>' + else: + for osv in Os: + generic = support_matrix.lookup(osv, arch, EditionKind.generic) + extended = support_matrix.lookup(osv, arch, EditionKind.extended) + result += f'<td markdown="span">{compatibility_editions(True if generic and generic.binary_supported else None, True if extended and extended.binary_supported else None, osv)}</td>' + result += f'<td markdown="span">{compatibility_editions(True if generic and generic.image_supported else None, True if extended and extended.image_supported else None, osv)}</td>' + + result += '<tr>' + + result += '</tbody>' + result += '</table>' + + return result + + @env.macro + def dist(os: Os | str, edition: EditionKind | str | None = None) -> str: + if type(os) is str: + os = Os[os] + if type(edition) is str: + edition = EditionKind[edition] + + files: Files = env.variables.files + file: File = files.get_file_from_path("setup/distribution.md") + dst = PurePath(path.relpath(file.src_path, path.dirname(env.page.file.src_path))) + if edition is None: + return f"[`{os.name}`]({dst.as_posix()}#{os.name}){{. class=dist-ref}}" + else: + return f"[`{os.name}`/`{edition.name}`]({dst.as_posix()}#{os.name}-{edition.name}){{. class=dist-edition-ref}}" + + @env.macro + def else_ref() -> str: + return "<span class=\"else-ref\">anything else</span>" @env.macro def escape_html(given: str) -> str: diff --git a/docs/assets/extra.css b/docs/assets/extra.css index f046e33..3ae20f5 100644 --- a/docs/assets/extra.css +++ b/docs/assets/extra.css @@ -58,6 +58,9 @@ p.bifroest-logo .switchable rect { [data-supported=true] { color: #309c30; } +[data-supported=none] { + color: var(--md-default-fg-color--light); +} [data-supported=false] { color: #dc6a43; } @@ -68,8 +71,21 @@ p.bifroest-logo .switchable rect { .md-typeset > .admonition:not(.subtitle), .md-typeset > .highlight:not(.subtitle), .md-typeset > .md-typeset__table:not(.subtitle), +.md-typeset > .md-typeset__scrollwrap > .md-typeset__table:not(.subtitle), .md-typeset > blockquote:not(.subtitle) { - margin-left: 1em; + margin-left: 12px; +} + +.md-typeset > .md-typeset__table:not(.subtitle), + .md-typeset > .md-typeset__scrollwrap > .md-typeset__table:not(.subtitle) { + padding-left: 0; +} + +html .md-typeset .admonition > :last-child { + margin-bottom: 0.5em; +} +html .md-typeset .admonition > :not(.admonition-title):first-child { + margin-top: 0.5em; } .md-typeset > .md-typeset__scrollwrap:not(.subtitle) { @@ -191,6 +207,10 @@ footer .md-footer-meta.md-typeset ul.md-footer-links { gap: 0.5em; } +footer .md-footer-meta.md-typeset .md-footer-links__container { + width: unset; +} + footer .md-footer-meta.md-typeset ul.md-footer-links > li { margin: 0; padding: 0; @@ -221,3 +241,30 @@ footer .md-footer-meta.md-typeset ul.md-footer-links > li:last-child::after { flex-direction: column-reverse; } } + +.md-typeset table:not([class]) th { + min-width: unset; +} + +.md-typeset table thead th > .dist-edition-ref, +.md-typeset table thead th > .else-ref { + writing-mode: vertical-lr; +} + +.md-typeset table[data-kind=compatibility_matrix] th, +.md-typeset table[data-kind=compatibility_matrix] td { + padding: 0.75em 1em; +} + +.md-typeset table[data-kind=compatibility_matrix] thead tr:first-child th:first-child { + vertical-align: bottom; +} + +.md-typeset table[data-kind=compatibility_matrix] thead tr:first-child th:not(:first-child) { + padding-bottom: 0; + text-align: center; +} + +.md-typeset table[data-kind=compatibility_matrix] thead tr:last-child th { + padding-top: 0.5em; +} diff --git a/docs/reference/authorization/htpasswd.md b/docs/reference/authorization/htpasswd.md index 6115405..272534b 100644 --- a/docs/reference/authorization/htpasswd.md +++ b/docs/reference/authorization/htpasswd.md @@ -84,6 +84,6 @@ This authorization will produce a context of type [Authorization Htpasswd](../co ## Compatibility -| [`linux`/`generic`](../../setup/distribution.md#linux-generic) | [`linux`/`extended`](../../setup/distribution.md#linux-extended) | [`windows`/`generic`](../../setup/distribution.md#windows-generic) | -| - | - | - | -| <<compatibility(True)>> | <<compatibility(True)>> | <<compatibility(True)>> | +| <<dist("linux")>> | <<dist("windows")>> | +| - | - | +| <<compatibility_editions(True,True,"linux")>> | <<compatibility_editions(True,None,"windows")>> | diff --git a/docs/reference/authorization/local.md b/docs/reference/authorization/local.md index 03cf028..b54667a 100644 --- a/docs/reference/authorization/local.md +++ b/docs/reference/authorization/local.md @@ -25,7 +25,7 @@ If set to a non-empty value, this [PAM](https://wiki.archlinux.org/title/PAM) se ##### Default settings -| [`linux`/`extended`](../../setup/distribution.md#linux-extended) | anything else | +| <<dist("linux","extended")>> | <<else_ref()>> | | - | - | | `sshd` | _empty_ | @@ -33,10 +33,6 @@ If set to a non-empty value, this [PAM](https://wiki.archlinux.org/title/PAM) se The password can either be validated via `/etc/passwd` and `/etc/shadow` (default) or via PAM (if [`pamService`](#property-pamService) is set to a valid value). -### Support of yescrypt {. #password-yescrypt} - -[yescrypt](https://en.wikipedia.org/wiki/Yescrypt) is cryptographic key derivation function used for password hashing in some modern Linux distributions (such as Ubuntu). Their support and give Bifröst the possibility to evaluate their passwords, the [`linux`/`extended` edition](../../setup/distribution.md#linux-extended) of Bifröst is required. - ### Properties {. #password-properties} <<property_with_holder("allowed", "Bool Template", "../templating/index.md#bool", "Context Password Authorization Request", "../context/authorization-request.md#password", default=True, id_prefix="password-", heading=4)>> @@ -59,8 +55,7 @@ This authorization will produce a context of type [Authorization Local](../conte ## Compatibility -| Feature | [`linux`/`generic`](../../setup/distribution.md#linux-generic) | [`linux`/`extended`](../../setup/distribution.md#linux-extended) | [`windows`/`generic`](../../setup/distribution.md#windows-generic) | -| - | - | - | - | -| [PAM](#property-pamService) | <<compatibility(False)>> | <<compatibility(True)>> | <<compatibility(False)>> | -| [yescrypt](#password-yescrypt) | <<compatibility(False)>> | <<compatibility(True)>> | <<compatibility(False)>> | -| anything else | <<compatibility(True)>> | <<compatibility(True)>> | <<compatibility(False)>> | +| Feature | <<dist("linux")>> | <<dist("windows")>> | +| - | - | - | +| [PAM](#property-pamService) | <<compatibility_editions(False,True,"linux")>> | <<compatibility_editions(False,None,"windows")>> | +| <<else_ref()>> | <<compatibility_editions(True,True,"windows")>> | <<compatibility_editions(False,None,"windows")>> | diff --git a/docs/reference/authorization/oidc.md b/docs/reference/authorization/oidc.md index 89a90bb..e10df43 100644 --- a/docs/reference/authorization/oidc.md +++ b/docs/reference/authorization/oidc.md @@ -71,6 +71,6 @@ scopes: ## Compatibility -| [`linux`/`generic`](../../setup/distribution.md#linux-generic) | [`linux`/`extended`](../../setup/distribution.md#linux-extended) | [`windows`/`generic`](../../setup/distribution.md#windows-generic) | -| - | - | - | -| <<compatibility(True)>> | <<compatibility(True)>> | <<compatibility(True)>> | +| <<dist("linux")>> | <<dist("windows")>> | +| - | - | +| <<compatibility_editions(True,True,"linux")>> | <<compatibility_editions(True,None,"windows")>> | diff --git a/docs/reference/authorization/simple.md b/docs/reference/authorization/simple.md index 2e5ca63..372d42e 100644 --- a/docs/reference/authorization/simple.md +++ b/docs/reference/authorization/simple.md @@ -63,6 +63,6 @@ This authorization will produce a context of type [Authorization Simple](../cont ## Compatibility -| [`linux`/`generic`](../../setup/distribution.md#linux-generic) | [`linux`/`extended`](../../setup/distribution.md#linux-extended) | [`windows`/`generic`](../../setup/distribution.md#windows-generic) | -| - | - | - | -| <<compatibility(True)>> | <<compatibility(True)>> | <<compatibility(True)>> | +| <<dist("linux")>> | <<dist("windows")>> | +| - | - | +| <<compatibility_editions(True,True,"linux")>> | <<compatibility_editions(True,None,"windows")>> | diff --git a/docs/reference/connection/ssh.md b/docs/reference/connection/ssh.md index a3810d4..772d13e 100644 --- a/docs/reference/connection/ssh.md +++ b/docs/reference/connection/ssh.md @@ -25,7 +25,7 @@ How many different authentication methods a client can use before the connection <<property("maxConnections", "uint8", None, default=255)>> The maximum amount of parallel connections on this service. Every additional connection beyond will be rejected. -<<property_with_holder("banner", "String Template", "../templating/index.md#string", "Connection", "../context/connection.md", default='{{ `/etc/ssh/sshd-banner` | file `optional` | default `Transcend with Engity Bifröst\n\n` }}')>> +<<property_with_holder("banner", "String Template", "../templating/index.md#string", "Connection", "../context/connection.md", default='{{ `/etc/ssh/sshd-banner` | file `optional` | default `Transcend with Engity\'s Bifröst\n\n` }}')>> Banner which will be shown when the client connects to the server even before the first validation of authorizations or similar happens. ## Examples @@ -83,6 +83,7 @@ rememberMeNotification: "If you return until {{.session.validUntil | format `dat ## Compatibility -| [`linux`/`generic`](../../setup/distribution.md#linux-generic) | [`linux`/`extended`](../../setup/distribution.md#linux-extended) | [`windows`/`generic`](../../setup/distribution.md#windows-generic) | -| - | - | - | -| <<compatibility(True)>> | <<compatibility(True)>> | <<compatibility(True)>> | +| <<dist("linux")>> | <<dist("windows")>> | +| - | - | +| <<compatibility_editions(True,True,"linux")>> | <<compatibility_editions(True,None,"windows")>> | + diff --git a/docs/reference/environment/local.md b/docs/reference/environment/local.md index 4db810f..a5cc084 100644 --- a/docs/reference/environment/local.md +++ b/docs/reference/environment/local.md @@ -322,6 +322,7 @@ If `true`, users are allowed to use SSH's port forwarding mechanism. ## Compatibility -| [`linux`/`generic`](../../setup/distribution.md#linux-generic) | [`linux`/`extended`](../../setup/distribution.md#linux-extended) | [`windows`/`generic`](../../setup/distribution.md#windows-generic) | -| - | - | - | -| <<compatibility(True)>> | <<compatibility(True)>> | <<compatibility(True)>> | +| <<dist("linux")>> | <<dist("windows")>> | +| - | - | +| <<compatibility_editions(True,True,"linux")>> | <<compatibility_editions(True,None,"windows")>> | + diff --git a/docs/reference/session/fs.md b/docs/reference/session/fs.md index f3b9e51..e469ca4 100644 --- a/docs/reference/session/fs.md +++ b/docs/reference/session/fs.md @@ -33,6 +33,7 @@ All files/directories inside the session storage will be stored with this mode. ## Compatibility -| [`linux`/`generic`](../../setup/distribution.md#linux-generic) | [`linux`/`extended`](../../setup/distribution.md#linux-extended) | [`windows`/`generic`](../../setup/distribution.md#windows-generic) | -| - | - | - | -| <<compatibility(True)>> | <<compatibility(True)>> | <<compatibility(True)>> | +| <<dist("linux")>> | <<dist("windows")>> | +| - | - | +| <<compatibility_editions(True,True,"linux")>> | <<compatibility_editions(True,None,"windows")>> | + diff --git a/docs/setup/distribution.md b/docs/setup/distribution.md index 6ff0e7c..59f161d 100644 --- a/docs/setup/distribution.md +++ b/docs/setup/distribution.md @@ -14,16 +14,26 @@ The generic Linux distribution of Bifröst contains features that run on every L ### Extended {: #linux-extended} -The extended Linux distribution of Bifröst currently only runs on Ubuntu 22.04+. It also requires some libraries that are installed: +The extended Linux distribution of Bifröst currently only runs on Debian 12+, Ubuntu 22.04+ and Fedora 39+. -```shell -sudo apt install libpam0g -y -``` - -On the other hand, it provides additional features like: +It does provide the following features: 1. [PAM authentication](../reference/authorization/local.md#property-pamService) via [Local authorization](../reference/authorization/local.md) -2. Support of [yescrypt](../reference/authorization/local.md#password-yescrypt) for `/etc/shadow` files, used for [Local authorization](../reference/authorization/local.md). + +### Dependencies + +| Name | Shared-Lib | Version | +| - | - | - | +| [GNU C Library (glibc)](https://www.gnu.org/software/libc/) | `libc.so.6` | 2.34+ | +| [Linux PAM (Pluggable Authentication Modules for Linux)](https://github.com/linux-pam/linux-pam) | `libpam.so.0` | 1.4+ | + +#### Installation + +* **Debian/Ubuntu**: Usually installed by default, in some cases the following command might be necessary: + ```shell + sudo apt install libpam0g -y + ``` +* **RedHat/Fedora**: Already installed by default. ## Windows {: #windows} @@ -31,14 +41,14 @@ On the other hand, it provides additional features like: The generic Windows distribution of Bifröst contains all supported features for Windows from Windows 7+ on. It does not even have any requirements on which other shared libraries need to be installed. ### Extended {: #windows-extended} -Currently, not available. +Not available. ## Matrix -| Architecture | [`linux`<br>`generic`](#linux-generic) | [`linux`<br>`extended`](#linux-extended) | [`windows`<br>`generic`](#windows-generic) | [`windows`<br>`extended`](#windows-extended) | -| - | - | - | - | - | -| `amd64` | :octicons-check-circle-24: | :octicons-check-circle-24: | :octicons-check-circle-24: | :octicons-circle-24: | -| `arm64` | :octicons-check-circle-24: | :octicons-check-circle-24: | :octicons-check-circle-24: | :octicons-circle-24: | +!!! tip "" + Cells express support in format of `<generic>`/`<extended>`. + +<<compatibility_matrix()>> ## Ways to obtain diff --git a/docs/setup/index.md b/docs/setup/index.md index 3a55068..7df49a2 100644 --- a/docs/setup/index.md +++ b/docs/setup/index.md @@ -16,12 +16,10 @@ toc_depth: 2 #### Matrix - > Column headers in format of `<os>`/`<edition>`. + !!! tip "" + Cells express support in format of `<generic>`/`<extended>`. - | `arch` | [`linux`/`generic`](distribution.md#linux-generic) | [`linux`/`extended`](distribution.md#linux-extended) | [`windows`/`generic`](distribution.md#windows-generic) | [`windows`/`extended`](distribution.md#windows-extended) | - | - | - | - | - | - | - | `amd64` | :octicons-check-circle-24: | :octicons-check-circle-24: | :octicons-check-circle-24: | :octicons-circle-24: | - | `arm64` | :octicons-check-circle-24: | :octicons-check-circle-24: | :octicons-check-circle-24: | :octicons-circle-24: | + <<compatibility_matrix()>> #### Example ```shell diff --git a/go.mod b/go.mod index 5f18b7f..cb5a84b 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.0 require ( github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 + github.com/Masterminds/semver/v3 v3.3.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/kingpin v2.2.6+incompatible github.com/coreos/go-oidc/v3 v3.11.0 @@ -12,17 +13,23 @@ require ( github.com/echocat/slf4g/native v1.6.1 github.com/fsnotify/fsnotify v1.7.0 github.com/gliderlabs/ssh v0.3.7 + github.com/google/go-containerregistry v0.20.2 + github.com/google/go-github/v65 v65.0.0 github.com/google/uuid v1.6.0 + github.com/gwatts/rootcerts v0.0.0-20240701182254-d67b2c3ed211 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 + github.com/mattn/go-zglob v0.0.6 github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a github.com/mr-tron/base58 v1.2.0 github.com/msteinert/pam/v2 v2.0.0 + github.com/opencontainers/image-spec v1.1.0 + github.com/openwall/yescrypt-go v1.0.0 github.com/otiai10/copy v1.14.0 github.com/pkg/sftp v1.13.6 github.com/shirou/gopsutil v3.21.11+incompatible github.com/stretchr/testify v1.9.0 github.com/tg123/go-htpasswd v1.2.2 - golang.org/x/crypto v0.28.0 + golang.org/x/crypto v0.27.0 golang.org/x/oauth2 v0.23.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -30,25 +37,34 @@ require ( require ( dario.cat/mergo v1.0.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect - github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 // indirect + github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect + github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v27.3.1+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect + github.com/klauspost/compress v1.17.10 // indirect github.com/kr/fs v0.1.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect - github.com/tklauser/numcpus v0.8.0 // indirect + github.com/tklauser/numcpus v0.9.0 // indirect + github.com/vbatts/tar-split v0.11.6 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.26.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index ba6065f..b1c9ace 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,12 @@ github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvr github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 h1:t3eaIm0rUkzbrIewtiFmMK5RXHej2XnoXNhxVsAYUfg= -github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= +github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= @@ -23,6 +25,12 @@ github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ= +github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/echocat/slf4g v1.6.1 h1:nQt0ZivDOsn8dyf3BJaHj1hWTF9MXsoefPUoTbVBI18= github.com/echocat/slf4g v1.6.1/go.mod h1:YvF/d1TcPvT+/xiHStLHPI4xPT1GGeEmPczn2MSljNA= github.com/echocat/slf4g/native v1.6.1 h1:g8Y8abLWwFkSQit4JQUvPO1MqpHUiPFZZINjkvPzY50= @@ -35,16 +43,28 @@ github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= +github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= +github.com/google/go-github/v65 v65.0.0 h1:pQ7BmO3DZivvFk92geC0jB0q2m3gyn8vnYPgV7GSLhQ= +github.com/google/go-github/v65 v65.0.0/go.mod h1:DvrqWo5hvsdhJvHd4WyVF9ttANN3BniqjP8uTFMNb60= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gwatts/rootcerts v0.0.0-20240701182254-d67b2c3ed211 h1:SWrkheqatqllDLJJ1VFDxwAauwXMpYyL0mhR4jjC5nU= +github.com/gwatts/rootcerts v0.0.0-20240701182254-d67b2c3ed211/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= +github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -54,36 +74,51 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A= +github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY= github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/msteinert/pam/v2 v2.0.0 h1:jnObb8MT6jvMbmrUQO5J/puTUjxy7Av+55zVJRJsCyE= github.com/msteinert/pam/v2 v2.0.0/go.mod h1:KT28NNIcDFf3PcBmNI2mIGO4zZJ+9RSs/At2PB3IDVc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/openwall/yescrypt-go v1.0.0 h1:jsGk48zkFvtUjGVOhYPGh+CS595JmTRcKnpggK2AON4= +github.com/openwall/yescrypt-go v1.0.0/go.mod h1:e6CWtFizUEOUttaOjeVMiv1lJaJie3mfOtLJ9CCD6sA= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -93,16 +128,18 @@ github.com/tg123/go-htpasswd v1.2.2 h1:tmNccDsQ+wYsoRfiONzIhDm5OkVHQzN3w4FOBAlN6 github.com/tg123/go-htpasswd v1.2.2/go.mod h1:FcIrK0J+6zptgVwK1JDlqyajW/1B4PtuJ/FLWl7nx8A= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= -github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= -github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= +github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= +github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -119,6 +156,7 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= @@ -126,8 +164,8 @@ golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -136,9 +174,12 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/mkdocs.yml b/mkdocs.yml index 0412437..84e8033 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,6 +32,7 @@ markdown_extensions: - tables - toc: toc_depth: 5 + - pymdownx.extra - pymdownx.caret - pymdownx.mark - pymdownx.tilde diff --git a/pkg/common/defer.go b/pkg/common/defer.go index a3ffe6f..39c6920 100644 --- a/pkg/common/defer.go +++ b/pkg/common/defer.go @@ -28,16 +28,22 @@ func IgnoreCloseError(when io.Closer) { } } -func DoOnFailure(assertToBeTrue *bool, otherwise func()) { +func DoIfFalse(assertToBeTrue *bool, otherwise func()) { if otherwise != nil && !*assertToBeTrue { otherwise() } } -func DoOnFailureIgnore(assertToBeTrue *bool, otherwise func() error) { - DoOnFailure(assertToBeTrue, func() { +func IgnoreErrorIfFalse(assertToBeTrue *bool, otherwise func() error) { + DoIfFalse(assertToBeTrue, func() { if otherwise != nil { _ = otherwise() } }) } + +func IgnoreCloseErrorIfFalse(assertToBeTrue *bool, otherwise io.Closer) { + if otherwise != nil { + IgnoreErrorIfFalse(assertToBeTrue, otherwise.Close) + } +} diff --git a/pkg/common/iterators.go b/pkg/common/iterators.go new file mode 100644 index 0000000..dcd9fbe --- /dev/null +++ b/pkg/common/iterators.go @@ -0,0 +1,74 @@ +package common + +import "iter" + +func JoinSeq[T any](seqs ...iter.Seq[T]) iter.Seq[T] { + return func(yield func(T) bool) { + for _, seq := range seqs { + for t := range seq { + if !yield(t) { + return + } + } + } + } +} + +func JoinSeq2[K any, V any](seqs ...iter.Seq2[K, V]) iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + for _, seq := range seqs { + for k, v := range seq { + if !yield(k, v) { + return + } + } + } + } +} + +func SeqOf[T any](ts ...T) iter.Seq[T] { + return func(yield func(T) bool) { + for _, t := range ts { + if !yield(t) { + return + } + } + } +} + +type KV[K any, V any] struct { + K K + V V +} + +func Seq2Of[K any, V any](kvs ...KV[K, V]) iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + for _, kv := range kvs { + if !yield(kv.K, kv.V) { + return + } + } + } +} + +func Seq2ErrOf[T any](ts ...T) iter.Seq2[T, error] { + return func(yield func(T, error) bool) { + for _, t := range ts { + if !yield(t, nil) { + return + } + } + } +} + +func SingleSeqOf[T any](t T) iter.Seq[T] { + return func(yield func(T) bool) { + yield(t) + } +} + +func SingleSeq2Of[K any, V any](k K, v V) iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + yield(k, v) + } +} diff --git a/pkg/configuration/configuration.go b/pkg/configuration/configuration.go index a2f3ce1..b114432 100644 --- a/pkg/configuration/configuration.go +++ b/pkg/configuration/configuration.go @@ -11,6 +11,10 @@ import ( "github.com/engity-com/bifroest/pkg/sys" ) +var ( + DefaultStartMessage = "" +) + type Configuration struct { Ssh Ssh `yaml:"ssh"` @@ -23,6 +27,8 @@ type Configuration struct { Flows Flows `yaml:"flows"` HouseKeeping HouseKeeping `yaml:"housekeeping"` + + StartMessage string `yaml:"startMessage,omitempty"` } func (this *Configuration) SetDefaults() error { @@ -31,6 +37,7 @@ func (this *Configuration) SetDefaults() error { func(v *Configuration) (string, defaulter) { return "session", &v.Session }, func(v *Configuration) (string, defaulter) { return "flows", &v.Flows }, func(v *Configuration) (string, defaulter) { return "houseKeeping", &v.HouseKeeping }, + fixedDefault("startMessage", func(v *Configuration) *string { return &v.StartMessage }, DefaultStartMessage), ) } @@ -40,6 +47,7 @@ func (this *Configuration) Trim() error { func(v *Configuration) (string, trimmer) { return "session", &v.Session }, func(v *Configuration) (string, trimmer) { return "flows", &v.Flows }, func(v *Configuration) (string, trimmer) { return "houseKeeping", &v.HouseKeeping }, + noopTrim[Configuration]("startMessage"), ) } @@ -50,6 +58,7 @@ func (this *Configuration) Validate() error { func(v *Configuration) (string, validator) { return "flows", &v.Flows }, notEmptySliceValidate("flows", func(v *Configuration) *[]Flow { return (*[]Flow)(&v.Flows) }), func(v *Configuration) (string, validator) { return "houseKeeping", &v.HouseKeeping }, + noopValidate[Configuration]("startMessage"), ) } @@ -111,5 +120,6 @@ func (this Configuration) isEqualTo(other *Configuration) bool { return isEqual(&this.Ssh, &other.Ssh) && isEqual(&this.Session, &other.Session) && isEqual(&this.Flows, &other.Flows) && - isEqual(&this.HouseKeeping, &other.HouseKeeping) + isEqual(&this.HouseKeeping, &other.HouseKeeping) && + this.StartMessage == other.StartMessage } diff --git a/pkg/crypto/ca-certs.crt b/pkg/crypto/ca-certs.crt new file mode 100644 index 0000000..bfacc77 --- /dev/null +++ b/pkg/crypto/ca-certs.crt @@ -0,0 +1,1698 @@ +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr +MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG +A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0 +MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp +Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD +QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz +i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8 +h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV +MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9 +UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni +8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC +h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD +VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB +AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm +KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ +X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr +QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5 +pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN +QSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF +eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx +MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV +BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog +D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS +sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop +O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk +sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi +c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj +VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz +KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ +TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G +sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs +1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD +fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN +l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ +VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 +c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp +4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s +t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj +2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO +vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C +xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx +cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM +fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw +RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY +BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz +MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u +LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 +v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd +e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw +V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA +AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG +GJTO +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL +BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x +FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx +MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s +THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc +IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU +AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ +GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 +8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH +flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt +J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim +0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN +pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ +UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW +OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB +AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet +8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j +bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM +Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv +TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS +S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr +I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 +b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB +UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P +Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven +sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICHTCCAaOgAwIBAgIUQ3CCd89NXTTxyq4yLzf39H91oJ4wCgYIKoZIzj0EAwMw +TjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29t +bVNjb3BlIFB1YmxpYyBUcnVzdCBFQ0MgUm9vdC0wMTAeFw0yMTA0MjgxNzM1NDNa +Fw00NjA0MjgxNzM1NDJaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2Nv +cGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgRUNDIFJvb3QtMDEw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAARLNumuV16ocNfQj3Rid8NeeqrltqLxeP0C +flfdkXmcbLlSiFS8LwS+uM32ENEp7LXQoMPwiXAZu1FlxUOcw5tjnSCDPgYLpkJE +hRGnSjot6dZoL0hOUysHP029uax3OVejQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSOB2LAUN3GGQYARnQE9/OufXVNMDAKBggq +hkjOPQQDAwNoADBlAjEAnDPfQeMjqEI2Jpc1XHvr20v4qotzVRVcrHgpD7oh2MSg +2NED3W3ROT3Ek2DS43KyAjB8xX6I01D1HiXo+k515liWpDVfG2XqYZpwI7UNo5uS +Um9poIyNStDuiw7LR47QjRE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICHDCCAaOgAwIBAgIUKP2ZYEFHpgE6yhR7H+/5aAiDXX0wCgYIKoZIzj0EAwMw +TjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29t +bVNjb3BlIFB1YmxpYyBUcnVzdCBFQ0MgUm9vdC0wMjAeFw0yMTA0MjgxNzQ0NTRa +Fw00NjA0MjgxNzQ0NTNaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2Nv +cGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgRUNDIFJvb3QtMDIw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAR4MIHoYx7l63FRD/cHB8o5mXxO1Q/MMDAL +j2aTPs+9xYa9+bG3tD60B8jzljHz7aRP+KNOjSkVWLjVb3/ubCK1sK9IRQq9qEmU +v4RDsNuESgMjGWdqb8FuvAY5N9GIIvejQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTmGHX/72DehKT1RsfeSlXjMjZ59TAKBggq +hkjOPQQDAwNnADBkAjAmc0l6tqvmSfR9Uj/UQQSugEODZXW5hYA4O9Zv5JOGq4/n +ich/m35rChJVYaoR4HkCMHfoMXGsPHED1oQmHhS48zs73u1Z/GtMMH9ZzkXpc2AV +mkzw5l4lIhVtwodZ0LKOag== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIUPgNJgXUWdDGOTKvVxZAplsU5EN0wDQYJKoZIhvcNAQEL +BQAwTjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwi +Q29tbVNjb3BlIFB1YmxpYyBUcnVzdCBSU0EgUm9vdC0wMTAeFw0yMTA0MjgxNjQ1 +NTRaFw00NjA0MjgxNjQ1NTNaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21t +U2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgUlNBIFJvb3Qt +MDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwSGWjDR1C45FtnYSk +YZYSwu3D2iM0GXb26v1VWvZVAVMP8syMl0+5UMuzAURWlv2bKOx7dAvnQmtVzslh +suitQDy6uUEKBU8bJoWPQ7VAtYXR1HHcg0Hz9kXHgKKEUJdGzqAMxGBWBB0HW0al +DrJLpA6lfO741GIDuZNqihS4cPgugkY4Iw50x2tBt9Apo52AsH53k2NC+zSDO3Oj +WiE260f6GBfZumbCk6SP/F2krfxQapWsvCQz0b2If4b19bJzKo98rwjyGpg/qYFl +P8GMicWWMJoKz/TUyDTtnS+8jTiGU+6Xn6myY5QXjQ/cZip8UlF1y5mO6D1cv547 +KI2DAg+pn3LiLCuz3GaXAEDQpFSOm117RTYm1nJD68/A6g3czhLmfTifBSeolz7p +UcZsBSjBAg/pGG3svZwG1KdJ9FQFa2ww8esD1eo9anbCyxooSU1/ZOD6K9pzg4H/ +kQO9lLvkuI6cMmPNn7togbGEW682v3fuHX/3SZtS7NJ3Wn2RnU3COS3kuoL4b/JO +Hg9O5j9ZpSPcPYeoKFgo0fEbNttPxP/hjFtyjMcmAyejOQoBqsCyMWCDIqFPEgkB +Ea801M/XrmLTBQe0MXXgDW1XT2mH+VepuhX2yFJtocucH+X8eKg1mp9BFM6ltM6U +CBwJrVbl2rZJmkrqYxhTnCwuwwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUN12mmnQywsL5x6YVEFm45P3luG0wDQYJ +KoZIhvcNAQELBQADggIBAK+nz97/4L1CjU3lIpbfaOp9TSp90K09FlxD533Ahuh6 +NWPxzIHIxgvoLlI1pKZJkGNRrDSsBTtXAOnTYtPZKdVUvhwQkZyybf5Z/Xn36lbQ +nmhUQo8mUuJM3y+Xpi/SB5io82BdS5pYV4jvguX6r2yBS5KPQJqTRlnLX3gWsWc+ +QgvfKNmwrZggvkN80V4aCRckjXtdlemrwWCrWxhkgPut4AZ9HcpZuPN4KWfGVh2v +trV0KnahP/t1MJ+UXjulYPPLXAziDslg+MkfFoom3ecnf+slpoq9uC02EJqxWE2a +aE9gVOX2RhOOiKy8IUISrcZKiX2bwdgt6ZYD9KJ0DLwAHb/WNyVntHKLr4W96ioD +j8z7PEQkguIBpQtZtjSNMgsSDesnwv1B10A8ckYpwIzqug/xBpMu95yo9GA+o/E4 +Xo4TwbM6l4c/ksp4qRyv0LAbJh6+cOx69TOY6lz/KwsETkPdY34Op054A5U+1C0w +lREQKC6/oAI+/15Z0wUOlV9TRe9rh9VIzRamloPh37MG88EU26fsHItdkJANclHn +YfkUyq+Dj7+vsQpZXdxc1+SWrVtgHdqul7I52Qb1dgAT+GhMIbA1xNxVssnBQVoc +icCMb3SgazNNtQEo/a2tiRc7ppqEvOuM6sRxJKi6KfkIsidWNTJf6jn7MZrVGczw +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIUVBa/O345lXGN0aoApYYNK496BU4wDQYJKoZIhvcNAQEL +BQAwTjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwi +Q29tbVNjb3BlIFB1YmxpYyBUcnVzdCBSU0EgUm9vdC0wMjAeFw0yMTA0MjgxNzE2 +NDNaFw00NjA0MjgxNzE2NDJaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21t +U2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgUlNBIFJvb3Qt +MDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDh+g77aAASyE3VrCLE +NQE7xVTlWXZjpX/rwcRqmL0yjReA61260WI9JSMZNRTpf4mnG2I81lDnNJUDMrG0 +kyI9p+Kx7eZ7Ti6Hmw0zdQreqjXnfuU2mKKuJZ6VszKWpCtYHu8//mI0SFHRtI1C +rWDaSWqVcN3SAOLMV2MCe5bdSZdbkk6V0/nLKR8YSvgBKtJjCW4k6YnS5cciTNxz +hkcAqg2Ijq6FfUrpuzNPDlJwnZXjfG2WWy09X6GDRl224yW4fKcZgBzqZUPckXk2 +LHR88mcGyYnJ27/aaL8j7dxrrSiDeS/sOKUNNwFnJ5rpM9kzXzehxfCrPfp4sOcs +n/Y+n2Dg70jpkEUeBVF4GiwSLFworA2iI540jwXmojPOEXcT1A6kHkIfhs1w/tku +FT0du7jyU1fbzMZ0KZwYszZ1OC4PVKH4kh+Jlk+71O6d6Ts2QrUKOyrUZHk2EOH5 +kQMreyBUzQ0ZGshBMjTRsJnhkB4BQDa1t/qp5Xd1pCKBXbCL5CcSD1SIxtuFdOa3 +wNemKfrb3vOTlycEVS8KbzfFPROvCgCpLIscgSjX74Yxqa7ybrjKaixUR9gqiC6v +wQcQeKwRoi9C8DfF8rhW3Q5iLc4tVn5V8qdE9isy9COoR+jUKgF4z2rDN6ieZdIs +5fq6M8EGRPbmz6UNp2YINIos8wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUR9DnsSL/nSz12Vdgs7GxcJXvYXowDQYJ +KoZIhvcNAQELBQADggIBAIZpsU0v6Z9PIpNojuQhmaPORVMbc0RTAIFhzTHjCLqB +KCh6krm2qMhDnscTJk3C2OVVnJJdUNjCK9v+5qiXz1I6JMNlZFxHMaNlNRPDk7n3 ++VGXu6TwYofF1gbTl4MgqX67tiHCpQ2EAOHyJxCDut0DgdXdaMNmEMjRdrSzbyme +APnCKfWxkxlSaRosTKCL4BWaMS/TiJVZbuXEs1DIFAhKm4sTg7GkcrI7djNB3Nyq +pgdvHSQSn8h2vS/ZjvQs7rfSOBAkNlEv41xdgSGn2rtO/+YHqP65DSdsu3BaVXoT +6fEqSWnHX4dXTEN5bTpl6TBcQe7rd6VzEojov32u5cSoHw2OHG1QAk8mGEPej1WF +sQs3BWDJVTkSBKEqz3EWnzZRSb9wO55nnPt7eck5HHisd5FUmrh1CoFSl+NmYWvt +PjgelmFV4ZFUjO2MJB+ByRCac5krFk5yAD9UG/iNuovnFNa2RU9g7Jauwy8CTl2d +lklyALKrdVwPaFsdZcJfMw8eD/A7hvWwTruc9+olBdytoptLFwG+Qt81IR2tq670 +v64fG9PiO/yzcnMcmyiQiRM9HcEARwmWmjgb3bHPDcK0RPOWlc4yOo80nOAXx17O +rg3bhzjlP1v9mxnhMUF6cKojawHhRUzNlM47ni3niAIi9G7oyOzWPPO5std3eqx7 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw +MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx +JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE +AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O +tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP +f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA +MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di +z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn +27iQ7t0l +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy +MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC +REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG +A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 +cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV +cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA +U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 +Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug +BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy +8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J +co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg +8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 +rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 +mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg ++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX +gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ +pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm +9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw +M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd +GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ +CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t +xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ +w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK +L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj +X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q +ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm +dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2WhcNNDcwMzMxMDkwMTM2WjBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zf +e9MEkVz6iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6C +cyvHZpsKjECcfIr28jlgst7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FDY1w8ndYn81LsF7Kpryz3dvgwHQYDVR0O +BBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjO +PQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFw +hVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dG +XSaQpYXFuXqUPoeovQA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgw +NTM2NDZaFw00MDA0MDgwNTM2NDZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6OcE3emhF +KxS06+QT61d1I02PJC0W6K6OyX2kVzsqdiUzg2zqMoqUm048luT9Ub+ZyZN+v/mt +p7JIKwccJ/VMvHASd6SFVLX9kHrko+RRWAPNEHl57muTH2SOa2SroxPjcf59q5zd +J1M3s6oYwlkm7Fsf0uZlfO+TvdhYXAvA42VvPMfKWeP+bl+sg779XSVOKik71gur +FzJ4pOE+lEa+Ym6b3kaosRbnhW70CEBFEaCeVESE99g2zvVQR9wsMJvuwPWW0v4J +hscGWa5Pro4RmHvzC1KqYiaqId+OJTN5lxZJjfU+1UefNzFJM3IFTQy2VYzxV4+K +h9GtxRESOaCtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBRXNPN0zwRL1SXm8UC2LEzZLemgrTANBgkqhkiG9w0BAQsF +AAOCAQEAPrvbFxbS8hQBICw4g0utvsqFepq2m2um4fylOqyttCg6r9cBg0krY6Ld +mmQOmFxv3Y67ilQiLUoT865AQ9tPkbeGGuwAtEGBpE/6aouIs3YIcipJQMPTw4WJ +mBClnW8Zt7vPemVV2zfrPIpyMpcemik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA +8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPSvWKErI4cqc1avTc7bgoitPQV +55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhgaaaI5gdka9at/ +yOPiZwud9AzqVN/Ssq+xIvEg37xEHA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw +NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ +FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg +vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy +6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo +/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J +kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ +0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib +y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac +18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs +0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB +SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL +ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk +86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib +ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT +zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS +DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 +2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo +FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy +K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 +dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl +Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB +365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c +JRNItX+S +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw +UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM +dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy +NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl +cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 +IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 +wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR +ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT +9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp +4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 +bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- diff --git a/pkg/crypto/ca-certs.go b/pkg/crypto/ca-certs.go new file mode 100644 index 0000000..b66b271 --- /dev/null +++ b/pkg/crypto/ca-certs.go @@ -0,0 +1,47 @@ +package crypto + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + _ "embed" + "net/http" +) + +var ( + //go:embed ca-certs.crt + caCertsRaw []byte + + caCerts = func(raw []byte) *x509.CertPool { + raw = bytes.TrimSpace(raw) + if len(raw) == 0 { + result, err := x509.SystemCertPool() + if err != nil { + panic(err) + } + return result + } + result := x509.NewCertPool() + result.AppendCertsFromPEM(raw) + return result + }(caCertsRaw) +) + +func CaCerts() *x509.CertPool { + return caCerts.Clone() +} + +func AdjustTlsConfigWithCaCerts(tlsConfig *tls.Config) { + tlsConfig.RootCAs = CaCerts() +} + +func AdjustHttpTransportWithCaCerts(transport *http.Transport) { + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = new(tls.Config) + } + AdjustTlsConfigWithCaCerts(transport.TLSClientConfig) +} + +func init() { + AdjustHttpTransportWithCaCerts(http.DefaultTransport.(*http.Transport)) +} diff --git a/pkg/crypto/unix/password/yescrypt.go b/pkg/crypto/unix/password/yescrypt.go index cb9b697..3e48276 100644 --- a/pkg/crypto/unix/password/yescrypt.go +++ b/pkg/crypto/unix/password/yescrypt.go @@ -1,14 +1,10 @@ -//go:build cgo && linux && !without_yescrypt - package password -/* -#cgo LDFLAGS: -lcrypt -#include <stdlib.h> -#include <crypt.h> -*/ -import "C" -import "unsafe" +import ( + "bytes" + + "github.com/openwall/yescrypt-go" +) func init() { instance := &Yescrypt{} @@ -18,14 +14,11 @@ func init() { type Yescrypt struct{} func (p *Yescrypt) Validate(password string, hash []byte) (bool, error) { - cKey := C.CString(password) - defer C.free(unsafe.Pointer(cKey)) - cHash := C.CString(string(hash)) - defer C.free(unsafe.Pointer(cHash)) - - out := C.crypt(cKey, cHash) - - return C.GoString(out) == string(hash), nil + rehash, err := yescrypt.Hash([]byte(password), hash) + if err != nil { + return false, err + } + return bytes.Equal(rehash, hash), nil } func (p *Yescrypt) Name() string { diff --git a/pkg/crypto/unix/password/yescrypt_test.go b/pkg/crypto/unix/password/yescrypt_test.go new file mode 100644 index 0000000..9b0d307 --- /dev/null +++ b/pkg/crypto/unix/password/yescrypt_test.go @@ -0,0 +1,30 @@ +package password + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestYescrypt_Validate(t *testing.T) { + instance := Yescrypt{} + + { + actual, actualErr := instance.Validate("changeme!", []byte("$y$j9T$joV328FhBQB66mB66/3vm.$cNgBaMBYgW0JyUMQsfi/OVoXIE2iy9MDUchynBlKiNA")) + assert.NoError(t, actualErr) + assert.Equal(t, true, actual) + } + + { + actual, actualErr := instance.Validate("changeme!2", []byte("$y$j9T$joV328FhBQB66mB66/3vm.$cNgBaMBYgW0JyUMQsfi/OVoXIE2iy9MDUchynBlKiNA")) + assert.NoError(t, actualErr) + assert.Equal(t, false, actual) + } + + { + actual, actualErr := instance.Validate("changeme!", []byte("$y$j9T$joV328FhBQB66mB66/3vm.$cNgBaMBYgW0JyUMQsfi/OVoXIE2iy9MDUchynblKiNA")) + assert.NoError(t, actualErr) + assert.Equal(t, false, actual) + } + +} diff --git a/pkg/service/housekeeper.go b/pkg/service/housekeeper.go index af39f12..9ca43c1 100644 --- a/pkg/service/housekeeper.go +++ b/pkg/service/housekeeper.go @@ -26,7 +26,7 @@ func (this *houseKeeper) init(service *service) error { this.service = service var ctx context.Context ctx, this.contextCancel = context.WithCancel(context.Background()) - defer common.DoOnFailure(&success, this.contextCancel) + defer common.DoIfFalse(&success, this.contextCancel) var nextRunIn time.Duration if initialDelay := this.service.Configuration.HouseKeeping.InitialDelay; initialDelay.IsZero() { diff --git a/pkg/service/service.go b/pkg/service/service.go index 618c06b..5a4eef1 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net" + "strings" "sync" "sync/atomic" "syscall" @@ -55,6 +56,15 @@ func (this *Service) Run(ctx context.Context) (rErr error) { if ctx == nil { ctx = context.Background() } + + if msg := this.Configuration.StartMessage; msg != "" { + for _, line := range strings.Split(msg, "\n") { + if line = strings.TrimSpace(line); line != "" { + log.Warn(line) + } + } + } + svc, err := this.prepare() if err != nil { return err diff --git a/pkg/session/fs-repository.go b/pkg/session/fs-repository.go index 5a110a5..e4e96e5 100644 --- a/pkg/session/fs-repository.go +++ b/pkg/session/fs-repository.go @@ -805,7 +805,7 @@ func (this *FsRepository) deletePublicKey(ctx context.Context, flow configuratio if err != nil { return fmt.Errorf("cannot open session file (%q) of %v/%v for write: %w", fnBuf, flow, id, err) } - defer common.DoOnFailureIgnore(&renamed, func() error { return os.Remove(fnBuf) }) + defer common.IgnoreErrorIfFalse(&renamed, func() error { return os.Remove(fnBuf) }) defer common.KeepCloseError(&rErr, fBuf) nAdded := 0 diff --git a/pkg/sys/signal_unix.go b/pkg/sys/signal_unix.go index 8b48de7..5a60641 100644 --- a/pkg/sys/signal_unix.go +++ b/pkg/sys/signal_unix.go @@ -26,7 +26,6 @@ const ( SIGPWR = Signal(syscall.SIGPWR) SIGQUIT = Signal(syscall.SIGQUIT) SIGSEGV = Signal(syscall.SIGSEGV) - SIGSTKFLT = Signal(syscall.SIGSTKFLT) SIGSTOP = Signal(syscall.SIGSTOP) SIGSYS = Signal(syscall.SIGSYS) SIGTERM = Signal(syscall.SIGTERM) @@ -34,7 +33,6 @@ const ( SIGTSTP = Signal(syscall.SIGTSTP) SIGTTIN = Signal(syscall.SIGTTIN) SIGTTOU = Signal(syscall.SIGTTOU) - SIGUNUSED = Signal(syscall.SIGUNUSED) SIGURG = Signal(syscall.SIGURG) SIGUSR1 = Signal(syscall.SIGUSR1) SIGUSR2 = Signal(syscall.SIGUSR2) @@ -65,7 +63,6 @@ var ( "PWR": SIGPWR, "QUIT": SIGQUIT, "SEGV": SIGSEGV, - "STKFLT": SIGSTKFLT, "STOP": SIGSTOP, "SYS": SIGSYS, "TERM": SIGTERM, @@ -73,7 +70,6 @@ var ( "TSTP": SIGTSTP, "TTIN": SIGTTIN, "TTOU": SIGTTOU, - "UNUSED": SIGUNUSED, "URG": SIGURG, "USR1": SIGUSR1, "USR2": SIGUSR2, diff --git a/pkg/user/etc-colon-repository-handles.go b/pkg/user/etc-colon-repository-handles.go index fc1aef7..d8310a3 100644 --- a/pkg/user/etc-colon-repository-handles.go +++ b/pkg/user/etc-colon-repository-handles.go @@ -21,7 +21,7 @@ type etcColonRepositoryHandles struct { func (this *etcColonRepositoryHandles) init(owner *EtcColonRepository) error { success := false - defer common.DoOnFailureIgnore(&success, this.close) + defer common.IgnoreErrorIfFalse(&success, this.close) this.owner = owner @@ -44,7 +44,7 @@ func (this *etcColonRepositoryHandles) open(rw bool) (_ *openedEtcColonRepositor var result openedEtcColonRepositoryHandles var err error - defer common.DoOnFailureIgnore(&success, result.close) + defer common.IgnoreErrorIfFalse(&success, result.close) if rw { directories := this.getDirectories() @@ -90,7 +90,7 @@ func (this *etcColonRepositoryHandles) openFile(fn string, rw bool, isCreateRetr } return nil, fmt.Errorf("cannot open %q: %w", fn, err) } - defer common.DoOnFailureIgnore(&success, result.Close) + defer common.IgnoreErrorIfFalse(&success, result.Close) if err := this.lockFile(result, lm); err != nil { return nil, err diff --git a/pkg/user/etc-colon-repository.go b/pkg/user/etc-colon-repository.go index 16aa07b..a3a79df 100644 --- a/pkg/user/etc-colon-repository.go +++ b/pkg/user/etc-colon-repository.go @@ -146,7 +146,7 @@ func (this *EtcColonRepository) Init(ctx context.Context) error { if err := this.handles.init(this); err != nil { return err } - defer common.DoOnFailureIgnore(&success, this.handles.close) + defer common.IgnoreErrorIfFalse(&success, this.handles.close) if err := this.load(ctx); err != nil { return err @@ -156,7 +156,7 @@ func (this *EtcColonRepository) Init(ctx context.Context) error { if err != nil { return fmt.Errorf("cannot initialize file watcher: %w", err) } - defer common.DoOnFailureIgnore(&success, watcher.Close) + defer common.IgnoreErrorIfFalse(&success, watcher.Close) go this.watchForChanges(watcher) diff --git a/pkg/user/etc-colon-repository_test.go b/pkg/user/etc-colon-repository_test.go index 611893c..b437f49 100644 --- a/pkg/user/etc-colon-repository_test.go +++ b/pkg/user/etc-colon-repository_test.go @@ -418,6 +418,7 @@ bar:XbarX:20453:10:100:::20818:`, groupFile := dir.file("group").setContent(c.group) shadowFile := dir.file("shadow").setContent(c.shadow) + var asyncError error instance := EtcColonRepository{ PasswdFilename: passwdFile.name(), GroupFilename: groupFile.name(), @@ -426,6 +427,11 @@ bar:XbarX:20453:10:100:::20818:`, AllowBadLine: &c.allowBadLine, OnUnhandledAsyncError: c.onUnhandledAsyncError, } + if instance.OnUnhandledAsyncError == nil { + instance.OnUnhandledAsyncError = func(_ log.Logger, err error, _ string) { + asyncError = err + } + } actualErr := instance.Init(context.Background()) if expectedErr := c.expectedError; expectedErr != "" { @@ -440,6 +446,8 @@ bar:XbarX:20453:10:100:::20818:`, actualErr = instance.Close() require.NoError(t, actualErr) + + require.NoError(t, asyncError) }) } } @@ -1454,6 +1462,7 @@ expired-ts:$y$j9T$as2ASyXW241FbtyMlNNQU1$sy6H9k6uXgaY1DeIKI5zPVsczWLD82k5UeQVuIM require.NoError(t, actualErr) assert.NoError(t, syncError) + time.Sleep(150 * time.Millisecond) }) } }