Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
devel:kubic
paranoia
paranoia-0.2.1.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File paranoia-0.2.1.obscpio of Package paranoia
07070100000000000081A40000000000000000000000016357C4F70000000F000000000000000000000000000000000000001B00000000paranoia-0.2.1/.cobra.yamluseViper: true 07070100000001000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000001700000000paranoia-0.2.1/.github07070100000002000081A40000000000000000000000016357C4F7000000CD000000000000000000000000000000000000002600000000paranoia-0.2.1/.github/dependabot.ymlversion: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" 07070100000003000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000002100000000paranoia-0.2.1/.github/workflows07070100000004000081A40000000000000000000000016357C4F70000077D000000000000000000000000000000000000002E00000000paranoia-0.2.1/.github/workflows/publish.yamlname: Publish on: push: branches: [main] tags: - "v*.*.*" env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} CONTAINER_TAR: "container.tar" jobs: paranoia-inception: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Log in to the Container registry uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: "Checkout code" uses: actions/checkout@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@57396166ad8aefe6098280995947635806a0e6ea with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=edge,branch=main type=ref,event=tag - name: Build and export to Docker uses: docker/build-push-action@v3 with: context: . load: true cache-from: type=gha cache-to: type=gha,mode=max outputs: type=docker,dest=${{ env.CONTAINER_TAR }} - name: "Run Paranoia container" uses: ./ with: target_tar: file://${{ env.CONTAINER_TAR }} - name: Build and push uses: docker/build-push-action@v3 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max 07070100000005000081A40000000000000000000000016357C4F700000660000000000000000000000000000000000000002E00000000paranoia-0.2.1/.github/workflows/release.yamlname: Release on: push: tags: - "v*.*.*" jobs: build: runs-on: ubuntu-latest strategy: matrix: os: [ linux, darwin ] arch: [ amd64, arm64 ] steps: - name: Install Go uses: actions/setup-go@v3 with: go-version: 1.18.x - uses: actions/checkout@v3 - name: Go Build run: CGO_ENABLED=0 GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -a -installsuffix cgo -o paranoia . - uses: actions/upload-artifact@v3 with: name: paranoia-${{ matrix.os }}-${{ matrix.arch }} path: paranoia docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Documentation Build run: go run ./hack/generate-manual - uses: actions/upload-artifact@v3 with: name: man-pages path: man/ release: needs: - build runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v3 - uses: actions/download-artifact@v3 - name: Rename Artifacts run: | mkdir bin mv paranoia-darwin-amd64/paranoia bin/paranoia-darwin-amd64 mv paranoia-darwin-arm64/paranoia bin/paranoia-darwin-arm64 mv paranoia-linux-amd64/paranoia bin/paranoia-linux-amd64 mv paranoia-linux-arm64/paranoia bin/paranoia-linux-arm64 mkdir man mv man-pages/* man/ - name: Release uses: softprops/action-gh-release@v1 with: files: | LICENSE.txt bin/paranoia-* man/* 07070100000006000081A40000000000000000000000016357C4F700000A87000000000000000000000000000000000000002B00000000paranoia-0.2.1/.github/workflows/test.yamlname: Test on: push: branches: - 'main' pull_request: branches: - '*' jobs: test: runs-on: ubuntu-latest steps: - name: Install Go uses: actions/setup-go@v3 with: go-version: 1.18.x - run: go install golang.org/x/tools/cmd/goimports@v0.1.12 - name: Checkout repository uses: actions/checkout@v3 - uses: actions/cache@v3 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Test run: go test ./... - name: Format run: go fmt ./... - name: Vet run: go vet ./... - run: goimports -w . - name: Verify No Changes id: verify-no-changes uses: tj-actions/verify-changed-files@v12.0 - name: Fail If Changes if: steps.verify-no-changes.outputs.files_changed == 'true' run: "false" integration-test: runs-on: ubuntu-latest steps: - name: Install Go uses: actions/setup-go@v3 with: go-version: 1.18.x - name: Checkout repository uses: actions/checkout@v3 - uses: actions/cache@v3 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Install run: go install . - name: Export Command Test run: it/export.sh docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Documentation Build run: go run ./hack/generate-manual - uses: actions/upload-artifact@v3 with: name: man-pages path: man/ # This action tests two things: That the GitHub Action works (as defined in the action.yml file) and by running # Paranoia on itself that we are shipping only the correct certs internally. paranoia-action-self-check: runs-on: ubuntu-latest env: CONTAINER_TAR: container.tar steps: - name: "Checkout code" uses: actions/checkout@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Build and export to Docker uses: docker/build-push-action@v3 with: context: . load: true cache-from: type=gha cache-to: type=gha,mode=max outputs: type=docker,dest=${{ env.CONTAINER_TAR }} - name: Paranoia Self-Check uses: ./ with: target_tar: file://${{ env.CONTAINER_TAR }} 07070100000007000081A40000000000000000000000016357C4F70000002E000000000000000000000000000000000000001A00000000paranoia-0.2.1/.gitignore# binary paranoia .paranoia.yaml /.idea man/ 07070100000008000081A40000000000000000000000016357C4F7000000C2000000000000000000000000000000000000001E00000000paranoia-0.2.1/.paranoia.yamlversion: "1" require: - fingerprints: sha256: 4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161 comment: "DigiCert Global Root, required for fetching Mozilla CA list" 07070100000009000081A40000000000000000000000016357C4F70000029B000000000000000000000000000000000000001A00000000paranoia-0.2.1/DockerfileFROM golang:1.18-alpine as builder WORKDIR /go/src/github.com/jetstack/paranoia # Download necessary Go modules COPY ./go.mod ./ COPY ./go.sum ./ RUN go mod download # Copy the files into the container COPY main.go main.go COPY ./cmd cmd COPY ./internal internal # Setup tmp directory RUN mkdir /new_tmp # Build the binary RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o paranoia . # Build tiny container FROM scratch COPY --from=builder /new_tmp /tmp COPY --from=builder /go/src/github.com/jetstack/paranoia/paranoia . ADD https://cacerts.digicert.com/DigiCertGlobalRootCA.crt.pem /etc/ssl/certs/DigiCertGlobalRootCA.crt ENTRYPOINT ["/paranoia"] 0707010000000A000081A40000000000000000000000016357C4F700002C5E000000000000000000000000000000000000001B00000000paranoia-0.2.1/LICENSE.txt Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 0707010000000B000081A40000000000000000000000016357C4F70000154B000000000000000000000000000000000000001900000000paranoia-0.2.1/README.md# Paranoia _Who do you trust?_ Paranoia is a tool to analyse and export trust bundles (e.g., "ca-certificates") from container images. These certificates identify the certificate authorities that your container trusts when establishing TLS connections. The design of TLS is that any certificate authority that your container trusts can issue a certificate for any domain. This means that a malicious or compromised certificate authority could issue a certificate to impersonate any other service, including your internal infrastructure. Paranoia can be used to inspect and validate the certificates within your container images. This gives you visibility into which certificate authorities your container images are trusting; allows you to forbid or require certificates at build-time in CI; and help you decide _who to trust_ in your container images. Paranoia is built by [Jetstack](https://jetstack.io) and made available under the Apache 2.0 license, see [LICENSE.txt](LICENSE.txt). ## Installation ### Binaries Binaries for common platforms and architectures are provided on the [releases](https://github.com/jetstack/paranoia/releases/latest). ### Go Install If you have [Go](https://go.dev/) installed you can install Paranoia using Go directly. ```shell go install github.com/jetstack/paranoia@latest ``` ## Examples Paranoia can be used to list out the certificates in a container image: ```shell $ paranoia export alpine:latest File Location Subject /etc/ssl/certs/ca-certificates.crt CN=ACCVRAIZ1,OU=PKIACCV,O=ACCV,C=ES /etc/ssl/certs/ca-certificates.crt OU=AC RAIZ FNMT-RCM,O=FNMT-RCM,C=ES /etc/ssl/certs/ca-certificates.crt CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS,OU=Ceres,O=FNMT-RCM,C=ES,2.5.4.97=#130f56415445532d51323832363030344a … /etc/ssl/certs/ca-certificates.crt CN=vTrus ECC Root CA,O=iTrusChina Co.\,Ltd.,C=CN /etc/ssl/certs/ca-certificates.crt CN=vTrus Root CA,O=iTrusChina Co.\,Ltd.,C=CN Found 140 certificates ``` Export them for further audit: ```shell paranoia export --output json python:3 | jq '.certificates[].fingerprintSHA256' | head -n 5 "ebd41040e4bb3ec742c9e381d31ef2a41a48b6685c96e7cef3c1df6cd4331c99" "6dc47172e01cbcb0bf62580d895fe2b8ac9ad4f873801e0c10b9c837d21eb177" "16af57a9f676b0ab126095aa5ebadef22ab31119d644ac95cd4b93dbf3f26aeb" "73c176434f1bc6d5adf45b0e76e727287c8de57616c1e6e6141a2b2cbc7d8e4c" "d7a7a0fb5d7e2731d771e9484ebcdef71d5f0c3e0a2948782bc83ee0ea699ef4" ``` Detect internal certificates left over from internal testing: ```shell cat << EOF > .paranoia.yaml version: "1" forbid: - comment: "An internal-only cert" fingerprints: sha256: bd40be0eccfce513ab318882f03962e4e2ec3799b51392e82805d9249e426d28 EOF paranoia validate my-image ``` Find certificates inside binaries: ```shell paranoia export -o json consul:latest | jq '.certificates[] | select(.fileLocation == "/bin/consul")' { "fileLocation": "/bin/consul", "owner": "CN=Circonus Certificate Authority,OU=Circonus,O=Circonus\\, Inc.,L=Columbia,ST=Maryland,C=US,1.2.840.113549.1.9.1=#0c0f636140636972636f6e75732e6e6574", "parser": "pem", "signature": "01C1B65D790706D2CAAD1D30406911D41884789A9D4FEBBCE31EE7B7628019A8C7B6643C46C1FDB684B18272B33880DAB68EB51C5546D731B9948C8A3D918890EC2F1CC8A751FAD1786BF2599FEEA17A63EB1997B577E8A65B9F67B368EA11B6C425F5D86A10C7BCCE02FBEA9F5867913AF409749A08A27D3B5EC8D8E332E216", "notBefore": "2009-12-23T19:17:06Z", "notAfter": "2019-12-21T19:17:06Z", "fingerprintSHA1": "063ff657e055b0036d794cda892c85417c07739a", "fingerprintSHA256": "0c97e0898343c5b1973c6568a15c8c853dd663d363020071e34f789859ece19f" } ``` ## Limitations Paranoia will detect certificate authorities in most cases, and is especially useful at finding accidental inclusion or for conducting a certificate authority inventory. However, there are some limitations to bear in mind while using Paranoia: - Paranoia only functions on container images, not running containers. Anything added into the container at runtime is not seen. - If a certificate is found, that doesn’t guarantee that the container will trust it as a certificate authority. It could, for example, be an unused leftover file. - It’s possible for an attacker to ‘hide’ a certificate authority from Paranoia (e.g., by encoding it in a format Paranoia doesn’t understand). In general Paranoia isn’t designed to defend against an adversary with supply chain write access intentionally sneaking obfuscated certificate authorities into container images. ## Usage The usage documentation for Paranoia is included in the help text. Invoke a command with `--help` for usage instructions, or see the manual pages. 0707010000000C000081A40000000000000000000000016357C4F700000408000000000000000000000000000000000000001A00000000paranoia-0.2.1/action.ymlname: Jetstack Paranoia description: | Validate the presence or absence of certificate authorities in your container image. inputs: target_tar: description: | Link to a .tar file of a container image to inspect. Should be on the format `file://<file>` required: true config: description: | Path to paranoia configuration file default: .paranoia.yaml permissive: description: | Permissive mode only validates that certificate authorities that are forbidden in your config file are not present, or ones that are required are present. All other certificate authorities are permitted. default: 'false' quiet: description: | On a validation failure, don't actually fail the action. default: 'false' branding: icon: award color: yellow runs: using: 'docker' image: 'Dockerfile' args: - validate - --config=${{ inputs.config }} - --permissive=${{ inputs.permissive }} - --quiet=${{ inputs.quiet }} - ${{ inputs.target_tar }} 0707010000000D000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000001300000000paranoia-0.2.1/cmd0707010000000E000081A40000000000000000000000016357C4F70000122F000000000000000000000000000000000000001D00000000paranoia-0.2.1/cmd/export.go// SPDX-License-Identifier: Apache-2.0 package cmd import ( "context" "encoding/hex" "encoding/json" "encoding/pem" "fmt" "os" "time" "github.com/fatih/color" "github.com/pkg/errors" "github.com/rodaine/table" "github.com/spf13/cobra" "github.com/jetstack/paranoia/cmd/options" "github.com/jetstack/paranoia/internal/image" "github.com/jetstack/paranoia/internal/output" ) func newExport(ctx context.Context) *cobra.Command { var ( imgOpts *options.Image outOpts *options.Output ) cmd := &cobra.Command{ Use: "export [flags] image", Short: "Export all certificate authorities in the given container image", Long: ` Exports all certificates found in the container image. The detail available depends on the output mode used In most output modes, partial certificates are also included after the main output. `, Example: ` Export certificates for an image: $ paranoia export alpine:latest Pipe certificate information into jq: $ paranoia export --output json alpine:latest | jq '.certificates[].fingerprintSHA256' `, PreRunE: func(_ *cobra.Command, args []string) error { if err := options.MustSingleImageArgs(args); err != nil { return err } return outOpts.Validate() }, RunE: func(cmd *cobra.Command, args []string) error { imageName := args[0] iOpts, err := imgOpts.Options() if err != nil { return errors.Wrap(err, "constructing image options") } parsedCertificates, err := image.FindImageCertificates(ctx, imageName, iOpts...) if err != nil { return err } if outOpts.Mode == options.OutputModePretty || outOpts.Mode == options.OutputModeWide { wide := outOpts.Mode == options.OutputModeWide headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc() columnFmt := color.New(color.FgYellow).SprintfFunc() var tbl table.Table if wide { tbl = table.New("File Location", "Parser", "Subject", "Not Before", "Not After", "SHA-256") } else { tbl = table.New("File Location", "Subject") } tbl.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt) for _, cert := range parsedCertificates.Found { if wide { tbl.AddRow(cert.Location, cert.Parser, cert.Certificate.Subject, cert.Certificate.NotBefore.Format(time.RFC3339), cert.Certificate.NotAfter.Format(time.RFC3339), hex.EncodeToString(cert.FingerprintSha256[:])) } else { tbl.AddRow(cert.Location, cert.Certificate.Subject) } } tbl.Print() fmt.Printf("Found %d certificates\n", len(parsedCertificates.Found)) if len(parsedCertificates.Partials) > 0 { headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc() columnFmt := color.New(color.FgYellow).SprintfFunc() tbl := table.New("File Location", "Parser", "Reason") tbl.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt) for _, p := range parsedCertificates.Partials { tbl.AddRow(p.Location, p.Parser, p.Reason) } tbl.Print() fmt.Printf("Found %d partial certificates\n", len(parsedCertificates.Partials)) } } else if outOpts.Mode == options.OutputModeJSON { var out output.JSONOutput for _, cert := range parsedCertificates.Found { out.Certificates = append(out.Certificates, output.JSONCertificate{ FileLocation: cert.Location, Owner: cert.Certificate.Subject.String(), Parser: cert.Parser, Signature: fmt.Sprintf("%X", cert.Certificate.Signature), NotBefore: cert.Certificate.NotBefore.Format(time.RFC3339), NotAfter: cert.Certificate.NotAfter.Format(time.RFC3339), FingerprintSHA1: hex.EncodeToString(cert.FingerprintSha1[:]), FingerprintSHA256: hex.EncodeToString(cert.FingerprintSha256[:]), }) } for _, p := range parsedCertificates.Partials { out.PartialCertificates = append(out.PartialCertificates, output.JSONPartialCertificate{ FileLocation: p.Location, Parser: p.Parser, Reason: p.Reason, }) } m, err := json.Marshal(out) if err != nil { return errors.Wrap(err, "failed to marshall output JSON") } fmt.Println(string(m)) } else if outOpts.Mode == options.OutputModePEM { for _, cert := range parsedCertificates.Found { pem.Encode(os.Stdout, &pem.Block{ Type: "CERTIFICATE", Bytes: cert.Certificate.Raw, }) } } return nil }, } imgOpts = options.RegisterImage(cmd) outOpts = options.RegisterOutputs(cmd) cmd.Args = cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs) return cmd } 0707010000000F000081A40000000000000000000000016357C4F700000B1B000000000000000000000000000000000000001E00000000paranoia-0.2.1/cmd/inspect.go// SPDX-License-Identifier: Apache-2.0 package cmd import ( "context" "fmt" "github.com/fatih/color" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/jetstack/paranoia/cmd/options" "github.com/jetstack/paranoia/internal/analyse" "github.com/jetstack/paranoia/internal/image" ) func newInspect(ctx context.Context) *cobra.Command { var imgOpts *options.Image cmd := &cobra.Command{ Use: "inspect [flags] image", Short: "Summarise potential issues with certificates", Long: ` Inspect prints out certificates that have one or more of the following faults: - Expired (based on current system time). - Close to expiry (based on current system time). - Removed by Mozilla from their certificate authority bundle. Partial certificates are also all printed for further inspection. `, PreRunE: func(_ *cobra.Command, args []string) error { return options.MustSingleImageArgs(args) }, RunE: func(cmd *cobra.Command, args []string) error { imageName := args[0] iOpts, err := imgOpts.Options() if err != nil { return errors.Wrap(err, "constructing image options") } parsedCertificates, err := image.FindImageCertificates(ctx, imageName, iOpts...) if err != nil { return err } analyser, err := analyse.NewAnalyser() if err != nil { return errors.Wrap(err, "failed to initialise analyser") } numIssues := 0 for _, cert := range parsedCertificates.Found { if cert.Certificate == nil { numIssues++ continue } notes := analyser.AnalyseCertificate(cert.Certificate) if len(notes) > 0 { numIssues++ fmt.Printf("Certificate %s\n", cert.Certificate.Subject) for i, n := range notes { var lead string if i == len(notes)-1 { lead = "┗" } else { lead = "┣" } var fmtFn func(format string, a ...interface{}) string var emoji string if n.Level == analyse.NoteLevelError { fmtFn = color.New(color.FgRed).SprintfFunc() emoji = "🚨" } else if n.Level == analyse.NoteLevelWarn { fmtFn = color.New(color.FgYellow).SprintfFunc() emoji = "⚠️" } fmt.Printf(lead + " " + fmtFn("%s %s\n", emoji, n.Reason)) } } } fmt.Printf("Found %d certificates total, of which %d had issues\n", len(parsedCertificates.Found), numIssues) if len(parsedCertificates.Partials) > 0 { for _, p := range parsedCertificates.Partials { fmtFn := color.New(color.FgYellow).SprintfFunc() fmt.Printf(fmtFn("⚠️ Partial certificate found in file %s: %s\n", p.Location, p.Reason)) } fmt.Printf("Found %d partial certificates\n", len(parsedCertificates.Partials)) } return nil }, } imgOpts = options.RegisterImage(cmd) cmd.Args = cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs) return cmd } 07070100000010000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000001B00000000paranoia-0.2.1/cmd/options07070100000011000081A40000000000000000000000016357C4F7000000DC000000000000000000000000000000000000002300000000paranoia-0.2.1/cmd/options/args.go// SPDX-License-Identifier: Apache-2.0 package options import "errors" func MustSingleImageArgs(args []string) error { if len(args) != 1 { return errors.New("expected single image name argument") } return nil } 07070100000012000081A40000000000000000000000016357C4F70000043F000000000000000000000000000000000000002400000000paranoia-0.2.1/cmd/options/image.go// SPDX-License-Identifier: Apache-2.0 package options import ( v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/jetstack/paranoia/internal/image" ) // Image contains options for interacting with images type Image struct { // Platform specifies the platform in the form // os/arch[/variant][:osversion] (e.g. linux/amd64) Platform string `json:"platform"` } // Options converts the options to a slice of image.Options func (i *Image) Options() ([]image.Option, error) { var opts []image.Option if i.Platform != "" { platform, err := v1.ParsePlatform(i.Platform) if err != nil { return []image.Option{}, errors.Wrap(err, "parsing platform string") } opts = append(opts, image.WithPlatform(platform)) } return opts, nil } // RegistryImage registers image options with cobra func RegisterImage(cmd *cobra.Command) *Image { var opts Image cmd.Flags().StringVar(&opts.Platform, "platform", "", "Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64)") return &opts } 07070100000013000081A40000000000000000000000016357C4F7000007FF000000000000000000000000000000000000002600000000paranoia-0.2.1/cmd/options/outputs.go// SPDX-License-Identifier: Apache-2.0 package options import ( "fmt" "strings" "github.com/spf13/cobra" ) const ( OutputModePretty = "pretty" OutputModeJSON = "json" OutputModeWide = "wide" OutputModePEM = "pem" ) var outputModes = []string{ OutputModePretty, OutputModeJSON, OutputModeWide, OutputModePEM, } // Output are options for configuring command outputs. type Output struct { // Mode is the output format of the command. Defaults to "pretty". Mode string `json:"format"` } func RegisterOutputs(cmd *cobra.Command) *Output { var opts Output cmd.Flags().StringVarP(&opts.Mode, "output", "o", "pretty", ` The output mode controls how Paranoia displays the data, and what data is shown. Supported modes are *pretty*, *wide*, *json*, and *pem*. *pretty*: Both certificates and partial certificates are output using a table to the terminal. This includes the file location (in the container) and the subject line of the certificate. *wide*: Like pretty mode, this uses a table to format data. Wide includes additional columns including the SHA256 and other information. *json*: The JSON output mode emits only JSON to STDOUT. Therefore, it is suitable for piping either to file or into programs that consume JSON text. The output format will include a "certificates" key containing an array of certificate objects. Each certificate object will have keys for "fileLocation", "owner", "parser", "signature", "notBefore", "notAfter", "fingerprintSHA1", and "fingerprintSHA256". Optionally, the output will include a "partials" key containing an array of partial certificate objects. Partial certificate objects will have keys for "fileLocation", "reason", and "parser". *pem*: Emits every certificate found in PEM format. In this output mode, partial certificates are omitted. `) return &opts } func (o *Output) Validate() error { for _, m := range outputModes { if o.Mode == m { return nil } } return fmt.Errorf("invalid output mode %q, must be one of %s", o.Mode, strings.Join(outputModes, ", ")) } 07070100000014000081A40000000000000000000000016357C4F7000003EC000000000000000000000000000000000000002900000000paranoia-0.2.1/cmd/options/validation.gopackage options import "github.com/spf13/cobra" // Validation are options for configuring validation command. type Validation struct { // Config is the filepath location to the validation configuration. Config string `json:"config"` // Quiet suppresses non-zero exit codes on validation failures. Quiet bool `json:"quiet"` // Permissive allows any certificate that is not otherwise forbidden. This // overrides the config's allow list. Permissive bool `json:"permissive"` } func RegisterValidation(cmd *cobra.Command) *Validation { var opts Validation cmd.PersistentFlags().StringVarP(&opts.Config, "config", "c", ".paranoia.yaml", "Path to configuration file for Paranoia's validate mode.") cmd.PersistentFlags().BoolVar(&opts.Quiet, "quiet", false, "Suppress nonzero exit code on validation failures.") cmd.PersistentFlags().BoolVar(&opts.Permissive, "permissive", false, "Allow any certificate that is not otherwise forbidden. This overrides the config's allow list.") return &opts } 07070100000015000081A40000000000000000000000016357C4F700000B69000000000000000000000000000000000000001B00000000paranoia-0.2.1/cmd/root.go// SPDX-License-Identifier: Apache-2.0 package cmd import ( "context" "fmt" "os" "github.com/spf13/cobra" "sigs.k8s.io/controller-runtime/pkg/manager/signals" ) func NewRoot(ctx context.Context) *cobra.Command { root := &cobra.Command{ Use: "paranoia subcommand", Short: "Inspect certificate authorities in container images ", Long: ` Paranoia is a command-line tool to inspect the certificate authorities present in a container image. It is capable of scanning not only well-known locations (such as PEM-encoded files under /etc/ssl/certs/), but finding certificates embedded in text files and even inside of binaries. ## LIMITATIONS Paranoia will detect certificate authorities in most cases, and is especially useful at finding accidental inclusion or for conducting a certificate authority inventory. However there are some limitations to bear in mind while using Paranoia: - Paranoia only functions on container images, not running containers. Anything added into the container at runtime is not seen. - If a certificate is found, that doesn’t guarantee that the container will trust it as a certificate authority. It could, for example, be an unused leftover file. - It’s possible for an attacker to ‘hide’ a certificate authority from Paranoia (e.g., by encoding it in a format Paranoia doesn’t understand). In general Paranoia isn’t designed to defend against an adversary with supply chain write access intentionally sneaking obfuscated certificate authorities into container images. ## CERTIFICATE DETECTION Paranoia runs a number of parsers over the data contained within a container image. This includes searching through files for strings, including binary files. Container images are comprised of layers. Each layer may remove or replace files from previous layers. Paranoia only considers the final state of the image, available to the application at runtime. Certificates in intermediate layers which are removed or replaced in later layers are not detected by Paranoia. ### Partial Certificates Paranoia can also detect "partial" certificates. A partial certificate is where Paranoia has detected data that appears to be a certificate but is incomplete or invalid. These can be false-positives, but are often worthy of further investigation. ## LOCAL IMAGES Paranoia can be invoked on any container image by name. This can include a tag, or a SHA256 fingerprint. Paranoia can also read from STDIN to handle local images that are exported as tar files. To enable this behaviour, use "-" as the image name. $ docker save my-local-image:sometag | paranoia export - `, } root.AddCommand(newExport(ctx)) root.AddCommand(newInspect(ctx)) root.AddCommand(newValidation(ctx)) return root } func Execute() { ctx := signals.SetupSignalHandler() if err := NewRoot(ctx).Execute(); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } 07070100000016000081A40000000000000000000000016357C4F70000189F000000000000000000000000000000000000001F00000000paranoia-0.2.1/cmd/validate.go// SPDX-License-Identifier: Apache-2.0 package cmd import ( "context" "fmt" "os" "strings" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/jetstack/paranoia/cmd/options" "github.com/jetstack/paranoia/internal/image" "github.com/jetstack/paranoia/internal/validate" ) func newValidation(ctx context.Context) *cobra.Command { var ( imgOpts *options.Image valOpts *options.Validation ) cmd := &cobra.Command{ Use: "validate [flags] image", Short: "Validate that the certificates in a container image conform to a provided config", Long: ` Check certificates found in a given container image against policy in a configuration file. If the policy is violated, then Paranoia will output issues to the command line and give a non-zero exit code. ## POLICY Paranoia can do three different things with certificates in this mode. Certificates are generally identified by either their SHA256 or SHA1 fingerprints. SHA256 is preferred where possible. ### Require If a certificate is required then Paranoia will fail if it is not present in the container. As a reminder, this does not guarantee that the program will correctly trust this certificate, just that it is present. ### Allow Allow a certificate, giving no error if it is found. Required certificates are implicitly allowed, there is no need to duplicate the entry. By default, Paranoia will error on any certificate not explicitly allowed (or required). The *--permissive* flag will disable this behaviour, and allow any certificate not explicitly forbidden. ### Forbid Forbid a certificate. Paranoia will always error if it finds a forbidden certificate in a container image. ## CONFIGURATION FILE The configuration file is a YAML formatted text file. By default Paranoia uses a file named .paranoia.yaml in the working directory, but the *--config* flag can be used to override this. This file should contain a "version" key at the root level. Presently this should be set to the string "1". Future versions of Paranoia may use different values for this key. Next it may contain the "require", "allow", and "forbid" keys. The behaviour of these keys is described above. Each of these keys is a list of certificate entries. Each certificate entry may contain the key "comment" with any commentary about the certificate. It must contain a "fingerprints" key, with one of "sha1" or "sha256" containing the SHA1 or SHA256 fingerprint of the certificate respectively. If both SHA1 and SHA256 fingerprints are given, the SHA1 is ignored.`, Example: ` An example configuration file: version: "1" require: - comment: "DigitCert Global Root" fingerprints: sha256: 4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161 allow: - comment: "ISRG X1 Root" fingerprints: sha256: 96bcec06264976f37460779acf28c5a7cfe8a3c0aae11a8ffcee05c0bddf08c6 forbid: - comment: "An internal-only cert" fingerprints: sha256: bd40be0eccfce513ab318882f03962e4e2ec3799b51392e82805d9249e426d28 Validating a locally built image, using the implicit .paranoia.yaml configuration file: $ docker build . -t example.com/image:v0.1.0 $ docker save example.com/image:v0.1.0 | paranoia validate - `, PreRunE: func(_ *cobra.Command, args []string) error { if err := options.MustSingleImageArgs(args); err != nil { return err } return nil }, RunE: func(cmd *cobra.Command, args []string) error { validateConfig, err := validate.LoadConfig(valOpts.Config) if err != nil { return errors.Wrap(err, "failed to load validator config") } validator, err := validate.NewValidator(*validateConfig, valOpts.Permissive) if err != nil { return errors.Wrap(err, "failed to initialise validator") } fmt.Println("Validating certificates with " + validator.DescribeConfig()) imageName := args[0] iOpts, err := imgOpts.Options() if err != nil { return errors.Wrap(err, "constructing image options") } // Validate operates only on full certificates, and ignores partials. parsedCertificates, err := image.FindImageCertificates(context.TODO(), imageName, iOpts...) if err != nil { return err } validateRes, err := validator.Validate(parsedCertificates.Found) if err != nil { return err } if validateRes.IsPass() { fmt.Printf("Scanned %d certificates in image %s, no issues found.\n", len(parsedCertificates.Found), imageName) } else { fmt.Printf("Scanned %d certificates in image %s, found issues.\n", len(parsedCertificates.Found), imageName) for _, na := range validateRes.NotAllowedCertificates { fmt.Printf("Certificate with SHA256 fingerprint %X in location %s was not allowed\n", na.FingerprintSha256, na.Location) } for _, f := range validateRes.ForbiddenCertificates { sb := strings.Builder{} sb.WriteString("Certificate with ") if f.Entry.Fingerprints.Sha1 != "" { sb.WriteString(fmt.Sprintf("SHA1 %X", f.Certificate.FingerprintSha1)) } else if f.Entry.Fingerprints.Sha256 != "" { sb.WriteString(fmt.Sprintf("SHA256 %X", f.Certificate.FingerprintSha256)) } sb.WriteString(fmt.Sprintf(" in location %s was forbidden!", f.Certificate.Location)) if f.Entry.Comment != "" { sb.WriteString(" Comment: ") sb.WriteString(f.Entry.Comment) } else { sb.WriteString(" No comment was provided.") } fmt.Println(sb.String()) } for _, req := range validateRes.RequiredButAbsent { sb := strings.Builder{} sb.WriteString("Certificate with ") if req.Fingerprints.Sha1 != "" { sb.WriteString(fmt.Sprintf("SHA1 %s", req.Fingerprints.Sha1)) } else if req.Fingerprints.Sha256 != "" { sb.WriteString(fmt.Sprintf("SHA256 %s", req.Fingerprints.Sha256)) } sb.WriteString(" was required, but was not found") if req.Comment != "" { sb.WriteString(" Comment: ") sb.WriteString(req.Comment) } else { sb.WriteString(" No comment was provided.") } fmt.Println(sb.String()) } if !valOpts.Quiet { os.Exit(1) } } return nil }, } imgOpts = options.RegisterImage(cmd) valOpts = options.RegisterValidation(cmd) cmd.Args = cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs) return cmd } 07070100000017000081A40000000000000000000000016357C4F700000639000000000000000000000000000000000000001600000000paranoia-0.2.1/go.modmodule github.com/jetstack/paranoia go 1.18 require ( github.com/fatih/color v1.13.0 github.com/google/go-cmp v0.5.9 github.com/google/go-containerregistry v0.12.0 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/pkg/errors v0.9.1 github.com/rodaine/table v1.0.1 github.com/spf13/cobra v1.6.0 github.com/stretchr/testify v1.8.1 gopkg.in/yaml.v3 v3.0.1 sigs.k8s.io/controller-runtime v0.13.0 ) require ( github.com/containerd/stargz-snapshotter/estargz v0.12.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/cli v20.10.20+incompatible // indirect github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/docker v20.10.20+incompatible // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/klauspost/compress v1.15.11 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/vbatts/tar-split v0.11.2 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.1.0 // indirect ) 07070100000018000081A40000000000000000000000016357C4F7000027CA000000000000000000000000000000000000001600000000paranoia-0.2.1/go.sumgithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/containerd/stargz-snapshotter/estargz v0.12.1 h1:+7nYmHJb0tEkcRaAW+MHqoKaJYZmkikupxCqVtmPuY0= github.com/containerd/stargz-snapshotter/estargz v0.12.1/go.mod h1:12VUuCq3qPq4y8yUW+l5w3+oXV3cx2Po3KSe/SmPGqw= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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 v20.10.20+incompatible h1:lWQbHSHUFs7KraSN2jOJK7zbMS2jNCHI4mt4xUFUVQ4= github.com/docker/cli v20.10.20+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v20.10.20+incompatible h1:kH9tx6XO+359d+iAkumyKDc5Q1kOwPuAUaeri48nD6E= github.com/docker/docker v20.10.20+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.12.0 h1:nidOEtFYlgPCRqxCKj/4c/js940HVWplCWc5ftdfdUA= github.com/google/go-containerregistry v0.12.0/go.mod h1:sdIK+oHQO7B93xI8UweYdl887YhuIwg9vz8BSLH3+8k= github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4= github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 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/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= 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-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rodaine/table v1.0.1 h1:U/VwCnUxlVYxw8+NJiLIuCxA/xa6jL38MY3FYysVWWQ= github.com/rodaine/table v1.0.1/go.mod h1:UVEtfBsflpeEcD56nF4F5AocNFta0ZuolpSVdPtlmP4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME= github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= 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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 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= k8s.io/apimachinery v0.25.0 h1:MlP0r6+3XbkUG2itd6vp3oxbtdQLQI94fD5gCS+gnoU= sigs.k8s.io/controller-runtime v0.13.0 h1:iqa5RNciy7ADWnIc8QxCbOX5FEKVR3uxVxKHRMc2WIQ= sigs.k8s.io/controller-runtime v0.13.0/go.mod h1:Zbz+el8Yg31jubvAEyglRZGdLAjplZl+PgtYNI6WNTI= 07070100000019000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000001400000000paranoia-0.2.1/hack0707010000001A000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000002400000000paranoia-0.2.1/hack/generate-manual0707010000001B000081A40000000000000000000000016357C4F7000001B9000000000000000000000000000000000000002E00000000paranoia-0.2.1/hack/generate-manual/manual.go// SPDX-License-Identifier: Apache-2.0 package main import ( "context" "log" "os" "github.com/spf13/cobra/doc" "github.com/jetstack/paranoia/cmd" ) func main() { header := &doc.GenManHeader{ Section: "1", } const path = "man/" _ = os.RemoveAll(path) err := os.Mkdir(path, 0755) if err != nil { log.Fatal(err) } err = doc.GenManTree(cmd.NewRoot(context.Background()), header, path) if err != nil { log.Fatal(err) } } 0707010000001C000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000001800000000paranoia-0.2.1/internal0707010000001D000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000002000000000paranoia-0.2.1/internal/analyse0707010000001E000081A40000000000000000000000016357C4F700000CA4000000000000000000000000000000000000002B00000000paranoia-0.2.1/internal/analyse/analyse.go// SPDX-License-Identifier: Apache-2.0 package analyse import ( "crypto/sha256" "crypto/x509" "encoding/csv" "fmt" "net/http" "time" "github.com/hako/durafmt" ) type NoteLevel string const ( NoteLevelWarn NoteLevel = "warn" NoteLevelError NoteLevel = "error" ) type Note struct { Level NoteLevel Reason string } type removedCertificate struct { Fingerprint string Comments string } type Analyser struct { RemovedCertificates []removedCertificate } // NewAnalyser creates a new Analyzer using the public Mozilla CA removed certificate list as part of // its checks. This method performs HTTP requests to retrieve that list. The request will be made with the given // context. func NewAnalyser() (*Analyser, error) { rc, err := downloadMozillaRemovedCACertsList() if err != nil { return nil, err } return &Analyser{RemovedCertificates: rc}, nil } func downloadMozillaRemovedCACertsList() ([]removedCertificate, error) { const mozillaRemovedCACertificateReportURL = "https://ccadb-public.secure.force.com/mozilla/RemovedCACertificateReportCSVFormat" resp, err := http.Get(mozillaRemovedCACertificateReportURL) if err != nil { return nil, err } csvReader := csv.NewReader(resp.Body) csvLines, err := csvReader.ReadAll() if err != nil { return nil, err } removedCerts := make([]removedCertificate, len(csvLines)) for i, csvLine := range csvLines { removedCerts[i] = removedCertificate{ // From the CSV format that Mozilla publishes, the 8th column (id 7) is the fingerprint and the 23rd column // (id 22) is the comment. Fingerprint: csvLine[7], Comments: csvLine[22], } } return removedCerts, nil } // AnalyseCertificate takes an X.509 certificate and performs basic analysis. This is intended to highlight any concerns // or issues to a user. func (an *Analyser) AnalyseCertificate(cert *x509.Certificate) []Note { now := time.Now() sixIshMonthsFromNow := now.Add(time.Hour * 24 * 30 * 6) var notes []Note if now.Before(cert.NotBefore) { notes = append(notes, Note{ Level: NoteLevelError, Reason: "not yet valid ( becomes valid on " + cert.NotBefore.Format(time.RFC3339) + " in " + fmtDuration(cert.NotBefore.Sub(now)) + ")", }) } if now.After(cert.NotAfter) { notes = append(notes, Note{ Level: NoteLevelError, Reason: "expired ( expired on " + cert.NotAfter.Format(time.RFC3339) + ", " + fmtDuration(now.Sub(cert.NotAfter)) + " since expiry)", }) } else if sixIshMonthsFromNow.After(cert.NotAfter) { notes = append(notes, Note{ Level: NoteLevelWarn, Reason: "expires soon ( expires on " + cert.NotAfter.Format(time.RFC3339) + ", " + fmtDuration(cert.NotAfter.Sub(now)) + " until expiry)", }) } fingerprint := fmt.Sprintf("%X", sha256.Sum256(cert.Raw)) for _, rc := range an.RemovedCertificates { if fingerprint == rc.Fingerprint { reason := "removed from Mozilla trust store" if rc.Comments == "" { reason += ", no reason given" } else { reason += ", comments: " + rc.Comments } notes = append(notes, Note{ Level: NoteLevelError, Reason: reason, }) } } return notes } func fmtDuration(duration time.Duration) string { return fmt.Sprint(durafmt.Parse(duration).LimitFirstN(2)) } 0707010000001F000081A40000000000000000000000016357C4F700000D21000000000000000000000000000000000000003000000000paranoia-0.2.1/internal/analyse/analyse_test.gopackage analyse import ( "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "fmt" "math/big" mathrand "math/rand" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAnalyser_AnalyseCertificate(t *testing.T) { // We don't use the NewAnalyser function, as that will do network access. Instead, construct an Analyser with a // predefined list of certificates. oneHourAgo := time.Now().Add(-time.Hour) // Okay, *approximately* one year. It doesn't matter if it's off by a few hours. inOneYear := time.Now().Add(time.Hour * 24 * 365) revokedCert, revokedFingerprint, err := generateTestCertificate(oneHourAgo, inOneYear) require.NoError(t, err) reasonString := "some string idk" analyser := Analyser{RemovedCertificates: []removedCertificate{ { Fingerprint: revokedFingerprint, Comments: reasonString, }, }} t.Run("revoked certificate", func(t *testing.T) { notes := analyser.AnalyseCertificate(revokedCert) assert.Len(t, notes, 1) assert.Equal(t, NoteLevelError, notes[0].Level) assert.Contains(t, notes[0].Reason, "removed from Mozilla trust store") assert.Contains(t, notes[0].Reason, reasonString) }) t.Run("expired certificate", func(t *testing.T) { expiredCert, _, err := generateTestCertificate(time.Now().Add(-time.Hour*2), time.Now().Add(-time.Hour)) require.NoError(t, err) notes := analyser.AnalyseCertificate(expiredCert) assert.Len(t, notes, 1) assert.Equal(t, NoteLevelError, notes[0].Level) assert.Contains(t, notes[0].Reason, "expired") }) t.Run("not yet valid certificate", func(t *testing.T) { expiredCert, _, err := generateTestCertificate(time.Now().Add(+time.Hour), inOneYear) require.NoError(t, err) notes := analyser.AnalyseCertificate(expiredCert) assert.Len(t, notes, 1) assert.Equal(t, NoteLevelError, notes[0].Level) assert.Contains(t, notes[0].Reason, "not yet valid") }) t.Run("expiring soon", func(t *testing.T) { expiredCert, _, err := generateTestCertificate(time.Now().Add(-time.Hour*2), time.Now().Add(time.Hour)) require.NoError(t, err) notes := analyser.AnalyseCertificate(expiredCert) assert.Len(t, notes, 1) assert.Equal(t, NoteLevelWarn, notes[0].Level) assert.Contains(t, notes[0].Reason, "expires soon") }) } // generateTestCertificate will generate a random test certificate. func generateTestCertificate(notBefore, notAfter time.Time) (*x509.Certificate, string, error) { ca := &x509.Certificate{ SerialNumber: big.NewInt(int64(mathrand.Int())), Subject: pkix.Name{ Organization: []string{"Jetstack"}, Country: []string{"UK"}, }, NotBefore: notBefore, NotAfter: notAfter, IsCA: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true, } caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { return nil, "", err } caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) if err != nil { return nil, "", err } cert, err := x509.ParseCertificate(caBytes) if err != nil { return nil, "", err } return cert, fmt.Sprintf("%X", sha256.Sum256(cert.Raw)), nil } 07070100000020000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000002400000000paranoia-0.2.1/internal/certificate07070100000021000081A40000000000000000000000016357C4F7000012B1000000000000000000000000000000000000003300000000paranoia-0.2.1/internal/certificate/certificate.go// SPDX-License-Identifier: Apache-2.0 package certificate import ( "archive/tar" "bytes" "context" "crypto/x509" "fmt" "io" "os" "path/filepath" "strings" "sync" ) // Found is a single X.509 certificate which was found by a parser inside the // given image. type Found struct { // Location is the filepath location where the certificate was found. Location string // Parser is the name of the parser which discovered the certificate. Parser string // Certificate is the parsed certificate. May be nil if the parser failed to // decode a found certificate. Certificate *x509.Certificate // Fingerprint is the SHA-1 fingerprint of the certificate. FingerprintSha1 [20]byte // Fingerprint is the SHA-256 fingerprint of the certificate. FingerprintSha256 [32]byte } // Partial is a "partial" certificate. Usually the result of parsing something that looks like a certificate but isn't // valid, or some other anomaly. These are often worthy of further investigation, but aren't compatible with Paranoia's // various certificate operations. type Partial struct { // Location is the filepath location where the certificate was found. Location string // Parser is the name of the parser which discovered the certificate. Parser string // Reason is a human-readable explanation of the certificate, either describe // why it couldn't be parsed or a summary of the parsed certificate. Reason string } type rseekerOpener func() (io.ReadSeeker, error) type ParsedCertificates struct { // Found is a slice of full, valid certificates we've found in the given container image. Found []Found // Partials is a slice of any partial certificates we've found. This might be fragments of certificates in memory // or other anomalies. Partials []Partial } func (p *ParsedCertificates) appendParsed(q *ParsedCertificates) { p.Found = append(p.Found, q.Found...) p.Partials = append(p.Partials, q.Partials...) } // parser is the interface implemented by X.509 certificate parsers. type parser interface { Find(context.Context, string, rseekerOpener) (*ParsedCertificates, error) } // FindCertificates will scan a container image, given as a file handler to a TAR file, for certificates and return them. func FindCertificates(ctx context.Context, imageTar io.Reader) (*ParsedCertificates, error) { var ( parsers = []parser{pem{}} parsed = &ParsedCertificates{} ) tz := tar.NewReader(imageTar) for { header, err := tz.Next() if err == io.EOF { break } if err != nil { return nil, err } // If file is not a regular file, ignore. if header.Typeflag != tar.TypeReg { continue } opener, oCleanup, err := openerForFile(ctx, header, tz) if err != nil { return nil, err } var ( wg sync.WaitGroup lock sync.Mutex errs []string ) wg.Add(len(parsers)) // Run all parsers. for _, p := range parsers { go func(p parser) { defer wg.Done() parserParsed, err := p.Find(ctx, filepath.Join("/", header.Name), opener) lock.Lock() defer lock.Unlock() if err != nil { errs = append(errs, err.Error()) } parsed.appendParsed(parserParsed) }(p) } wg.Wait() select { case <-ctx.Done(): return nil, ctx.Err() default: } if err := oCleanup(); err != nil { errs = append(errs, err.Error()) } if len(errs) > 0 { return parsed, fmt.Errorf("parser error finding certificates: %s", strings.Join(errs, "; ")) } } return parsed, nil } // openerForFile returns an rseekerOpener and clean-up function for the given // tarball file. Depending of the size of the file, the ReadSeeker will // ordinate from an in-memory buffer, or a temporary file. func openerForFile(ctx context.Context, header *tar.Header, reader io.Reader) (rseekerOpener, func() error, error) { // If file is larger than a Gig, write to a temporary file. if header.Size > (1 << 30) { tmp, err := os.CreateTemp(os.TempDir(), strings.ReplaceAll(filepath.Clean(header.Name), string(filepath.Separator), "-")) if err != nil { return nil, nil, fmt.Errorf("failed to create temporary file: %w", err) } if _, err := io.Copy(tmp, reader); err != nil { return nil, nil, fmt.Errorf("failed to write image file to temporary file: %w", err) } if err := tmp.Close(); err != nil { return nil, nil, fmt.Errorf("failed to close temporary file: %w", err) } return func() (io.ReadSeeker, error) { return os.Open(tmp.Name()) }, func() error { return os.Remove(tmp.Name()) }, nil } else { // Simple in-memory buffer. ff, err := io.ReadAll(reader) if err != nil { return nil, nil, fmt.Errorf("failed to read image file: %w", err) } return func() (io.ReadSeeker, error) { return bytes.NewReader(ff), nil }, func() error { return nil }, nil } } 07070100000022000081A40000000000000000000000016357C4F700000850000000000000000000000000000000000000003800000000paranoia-0.2.1/internal/certificate/certificate_test.gopackage certificate import ( "archive/tar" "bytes" "context" "fmt" "io" "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_openerForFile(t *testing.T) { t.Run("a large file should result in a file being written, which should be deleted when closed", func(t *testing.T) { unix := time.Now().Unix() name := fmt.Sprintf(" hello/world-file-%d ", unix) buf := bytes.NewReader([]byte("hello-world")) rsopener, closer, err := openerForFile(context.TODO(), &tar.Header{ Name: name, Size: 999999999999999999, }, buf) require.NoError(t, err) dir, err := os.ReadDir(os.TempDir()) require.NoError(t, err) var filename string for _, f := range dir { if strings.Contains(f.Name(), fmt.Sprintf("hello-world-file-%d", unix)) { filename = filepath.Join(os.TempDir(), f.Name()) break } } require.NotEmpty(t, filename) b, err := os.ReadFile(filename) require.NoError(t, err) assert.Equal(t, []byte("hello-world"), b) rs, err := rsopener() require.NoError(t, err) b, err = io.ReadAll(rs) require.NoError(t, err) assert.Equal(t, []byte("hello-world"), b) assert.NoError(t, closer()) assert.NoFileExists(t, filename) }) t.Run("a small file should result in no file being written", func(t *testing.T) { unix := time.Now().Unix() name := fmt.Sprintf(" hello/world-file-%d ", unix) buf := bytes.NewReader([]byte("hello-world")) rsopener, closer, err := openerForFile(context.TODO(), &tar.Header{ Name: name, Size: 10, }, buf) require.NoError(t, err) dir, err := os.ReadDir(os.TempDir()) require.NoError(t, err) var filename string for _, f := range dir { if strings.Contains(f.Name(), fmt.Sprintf("hello-world-file-%d", unix)) { filename = filepath.Join(os.TempDir(), f.Name()) break } } require.Empty(t, filename) rs, err := rsopener() require.NoError(t, err) b, err := io.ReadAll(rs) require.NoError(t, err) assert.Equal(t, []byte("hello-world"), b) assert.NoError(t, closer()) assert.NoFileExists(t, filename) }) } 07070100000023000081A40000000000000000000000016357C4F7000014F2000000000000000000000000000000000000002B00000000paranoia-0.2.1/internal/certificate/pem.go// SPDX-License-Identifier: Apache-2.0 package certificate import ( "bytes" "context" "crypto/sha1" "crypto/sha256" "crypto/x509" encpem "encoding/pem" "errors" "fmt" "io" ) type pem struct{} // Find finds X.509 PEM encoded certificates in the given reader. It does this // by greping through the input and attempting to find the PEM Certificate // header. Once found, it attempts to find the end footer. Even if the end // footer is not found, a Certificate is still recorded, but marked as not // correctly decoded. func (_ pem) Find(ctx context.Context, location string, rs rseekerOpener) (*ParsedCertificates, error) { ignored := []byte{'\n', '\t', '\r', ' ', '\f', '\v', '\b', '\x00', '"', '\''} pemStart := []byte("-----BEGIN CERTIFICATE-----") pemEnd := []byte("-----END CERTIFICATE-----") file, err := rs() if err != nil { return nil, err } var ( // token is the single token buffer we use to scan each file. token = make([]byte, 1) // results is the end result of the found certificates for this file // location. results []Found // partials is the list of partial certificates found partials []Partial // Current is the current successfully decoded certificate buffer. Starts // empty until we start to scan with a successful header. current []byte ) for { // Read a single token from the file. Exit scanning if we reach the end of // the file. _, err := file.Read(token) if errors.Is(err, io.EOF) { break } // Exit if we encounter an error reading from file. if err != nil { return nil, err } // If context has been cancelled, exit scanning. select { case <-ctx.Done(): return nil, ctx.Err() default: } // We ignore space tokens so allow for correctly scanning malformed // certificates. if bytes.Contains(ignored, token) { continue } if token[0] == pemStart[len(current)] { // If the scanned token matches the current PEM start token, we append to // the current. current = append(current, token[0]) } else { // Did not match, reset current to empty. current = current[:0] } if len(current) == 10 { // Make sure we add the space character from PEM start since those get // ignored. current = append(current, ' ') } // If we have the PEM header, then we can start to scan for the footer. if len(current) == len(pemStart) { // footer is the buffer we use to match on the PEM footer. var footer []byte for { // Check errors and return/break appropriately. _, err := file.Read(token) if errors.Is(err, io.EOF) { break } if err != nil { return nil, err } // Again, check context. select { case <-ctx.Done(): return nil, ctx.Err() default: } // continue for ignored characters. if bytes.Contains(ignored, token) { // Append if we haven't reached the footer yet, or need to add the // space character. if len(footer) == 0 || len(footer) == 9 { current = append(current, token[0]) } continue } // Always append to current to catch all certificate data. current = append(current, token[0]) // Check for PEM footer character, or reset footer. if token[0] == pemEnd[len(footer)] { footer = append(footer, token[0]) } else { footer = footer[:0] } // Add in the space character we ignore. if len(footer) == 8 { footer = append(footer, ' ') } // If the length of footer matches the PEM footer, then we have scanned // a certificate. if len(footer) == len(pemEnd) { //current = append(current, footer...) break } } // Here we stopped scanning for the footer. This might be because we got // to the end of the file, or we matched on the footer. var ( valid = false reason string cert *x509.Certificate fpsha1 [20]byte fpsha256 [32]byte ) // If we did match on the footer, then attempt to decode the actual // certificate. if len(footer) == len(pemEnd) { block, _ := encpem.Decode(current) if block == nil { reason = fmt.Sprintf("a block of data looks like a PEM certificate, but cannot be decoded") } else { cert, err = x509.ParseCertificate(block.Bytes) if err != nil { reason = fmt.Sprintf("failed to parse PEM certificate: %s", err) } else { fpsha1 = sha1.Sum(block.Bytes) fpsha256 = sha256.Sum256(block.Bytes) valid = true } } } else { // If we didn't actually decode an entire certificate, then set an // appropriate reason, and reset the file so we can re-scan. reason = "found start of PEM encoded certificate, but could not find end" if _, err := file.Seek(-int64(len(current)-len(pemStart)+1), io.SeekCurrent); err != nil { return nil, fmt.Errorf("failed to seek: %w", err) } } // Capture result. if valid { results = append(results, Found{ Location: location, Parser: "pem", Certificate: cert, FingerprintSha1: fpsha1, FingerprintSha256: fpsha256, }) } else { partials = append(partials, Partial{ Location: location, Parser: "pem", Reason: reason, }) } current = current[:0] } } return &ParsedCertificates{ Found: results, Partials: partials, }, nil } 07070100000024000081A40000000000000000000000016357C4F700000A1F000000000000000000000000000000000000003000000000paranoia-0.2.1/internal/certificate/pem_test.gopackage certificate import ( "bytes" "context" "io" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_x509pem(t *testing.T) { tests := map[string]struct { file string expSubjects []string expPartialReasons []string }{ "simple certificate list should parse": { file: "testdata/test-1", expSubjects: []string{ "CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US", "CN=Google Internet Authority G2,O=Google Inc,C=US", "CN=www.google.com,O=Google Inc,L=Mountain View,ST=California,C=US", }, }, "certificate list if splattering of new lines should parse": { file: "testdata/test-2", expSubjects: []string{ "CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US", "CN=Google Internet Authority G2,O=Google Inc,C=US", "CN=www.google.com,O=Google Inc,L=Mountain View,ST=California,C=US", }, }, "certificate starts, but doesn't end should be picked up": { file: "testdata/test-3", expSubjects: []string{ "CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US", "CN=Google Internet Authority G2,O=Google Inc,C=US", "CN=www.google.com,O=Google Inc,L=Mountain View,ST=California,C=US", "CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US", }, expPartialReasons: []string{ "a block of data looks like a PEM certificate, but cannot be decoded", }, }, "malformed certificates should still be reported": { file: "testdata/test-4", expSubjects: []string{ "CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US", "CN=www.google.com,O=Google Inc,L=Mountain View,ST=California,C=US", }, expPartialReasons: []string{ "failed to parse PEM certificate: x509: malformed certificate", }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { f, err := os.Open(test.file) require.NoError(t, err) parsedCerts, err := (pem{}).Find(context.TODO(), test.file, func() (io.ReadSeeker, error) { ff, err := io.ReadAll(f) if err != nil { return nil, err } return bytes.NewReader(ff), nil }) assert.NoError(t, err) var subjects []string for _, r := range parsedCerts.Found { assert.Equal(t, test.file, r.Location) subjects = append(subjects, r.Certificate.Subject.String()) } assert.ElementsMatch(t, test.expSubjects, subjects) var partialsReasons []string for _, r := range parsedCerts.Partials { assert.Equal(t, test.file, r.Location) partialsReasons = append(partialsReasons, r.Reason) } assert.ElementsMatch(t, test.expPartialReasons, partialsReasons) }) } } 07070100000025000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000002D00000000paranoia-0.2.1/internal/certificate/testdata07070100000026000081A40000000000000000000000016357C4F7000010B2000000000000000000000000000000000000003400000000paranoia-0.2.1/internal/certificate/testdata/test-1-----BEGIN CERTIFICATE----- MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU 1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG EwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy bmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP VaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv h8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE ahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ EASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC DTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7 qwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g K4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI KwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n ZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB BQUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY /iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/ zG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza HFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto WHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6 yuGnBXj8ytqU0CwIPX4WecigUCAkVDNx -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEdjCCA16gAwIBAgIIcR5k4dkoe04wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMzEyMDkzODMwWhcNMTQwNjEwMDAwMDAw WjBoMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEXMBUGA1UEAwwOd3d3 Lmdvb2dsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4zYCe m0oUBhwE0EwBr65eBOcgcQO2PaSIAB2dEP/c1EMX2tOy0ov8rk83ePhJ+MWdT1z6 jge9X4zQQI8ZyA9qIiwrKBZOi8DNUvrqNZC7fJAVRrb9aX/99uYOJCypIbpmWG1q fhbHjJewhwf8xYPj71eU4rLG80a+DapWmphtfq3h52lDQIBzLVf1yYbyrTaELaz4 NXF7HXb5YkId/gxIsSzM0aFUVu2o8sJcLYAsJqwfFKBKOMxUcn545nlspf0mTcWZ 0APlbwsKznNs4/xCDwIxxWjjqgHrYAFl6y07i1gzbAOqdNEyR24p+3JWI8WZBlBI dk2KGj0W1fIfsvyxAgMBAAGjggFBMIIBPTAdBgNVHSUEFjAUBggrBgEFBQcDAQYI KwYBBQUHAwIwGQYDVR0RBBIwEIIOd3d3Lmdvb2dsZS5jb20waAYIKwYBBQUHAQEE XDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0 MCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0G A1UdDgQWBBTXD5Bx6iqT+dmEhbFL4OUoHyZn8zAMBgNVHRMBAf8EAjAAMB8GA1Ud IwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMBcGA1UdIAQQMA4wDAYKKwYBBAHW eQIFATAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lB RzIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCR3RJtHzgDh33b/MI1ugiki+nl8Ikj 5larbJRE/rcA5oite+QJyAr6SU1gJJ/rRrK3ItVEHr9L621BCM7GSdoNMjB9MMcf tJAW0kYGJ+wqKm53wG/JaOADTnnq2Mt/j6F2uvjgN/ouns1nRHufIvd370N0LeH+ orKqTuAPzXK7imQk6+OycYABbqCtC/9qmwRd8wwn7sF97DtYfK8WuNHtFalCAwyi 8LxJJYJCLWoMhZ+V8GZm+FOex5qkQAjnZrtNlbQJ8ro4r+rpKXtmMFFhfa+7L+PA Kom08eUK8skxAzfDDijZPh10VtJ66uBoiDPdT+uCBehcBIcmSTrKjFGX -----END CERTIFICATE----- 07070100000027000081A40000000000000000000000016357C4F700001122000000000000000000000000000000000000003400000000paranoia-0.2.1/internal/certificate/testdata/test-2- ----BEGIN CERTIFICATE ----- MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU 1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== -----END CERTIFICATE----- jslfskdfjskljf - - ---BEGIN CERTI FICATE----- MIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG EwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy bmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP VaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv h8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE ahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ EASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC DTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7 qwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g K4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI KwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n ZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB B QUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY /iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/ zG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza HFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto WHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6 yuGnBXj8ytqU0CwIPX4WecigUCAkVDNx -- - --END CERTIFICATE----- echo "-----BEGIN CERTIFICATE----- M IIEdjCCA16gAwIBAgIIcR5k4dkoe04wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMzEyMDkzODMwWhcNMTQwNjEwMDAwMDAw WjBoMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEXMBUGA1UEAwwOd3d3 Lmdvb2dsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4zYCe m0oUBhwE0EwBr65eBOcgcQO2PaSIAB2dEP/c1EMX2tOy0ov8rk83ePhJ+MWdT1z6 jge9X4zQQI8ZyA9qIiwrKBZOi8DNUvrqNZC7fJAVRrb9aX/99uYOJCypIbpmWG1q fhbHjJewhwf8xYPj71eU4rLG80a+DapWmphtfq3h52lDQIBzLVf1yYbyrTaELaz4 NXF7HXb5YkId/gxIsSzM0aFUVu2o8sJcLYAsJqwfFKBKOMxUcn545nlspf0mTcWZ 0APlbwsKznNs4/xCDwIxxWjjqgHrYAFl6y07i1gzbAOqdNEyR24p+3JWI8WZBlBI dk2KGj0W1fIfsvyxAgMBAAGjggFBMIIBPTAdBgNVHSUEFjAUBggrBgEFBQcDAQYI KwYBBQUHAwIwGQYDVR0RBBIwEIIOd3d3Lmdvb2dsZS5jb20waAYIKwYBBQUHAQEE XDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0 MCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0G A1UdDgQWBBTXD5Bx6iqT+dmEhbFL4OUoHyZn8zAMBgNVHRMBAf8EAjAAMB8GA1Ud IwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMBcGA1UdIAQQMA4wDAYKKwYBBAHW eQIFATAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lB RzIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCR3RJtHzgDh33b/MI1ugiki+nl8Ikj 5larbJRE/rcA5oite+QJyAr6SU1gJJ/ rRrK3ItVEHr9L621BCM7GSdoNMjB9MMcf t JAW0kYGJ+wqKm53wG/JaOADTnnq2Mt/j6F2uvjgN/ouns1nRHufIvd370N0LeH+ orKqTuAPzXK7imQk6+OycYABbqCtC/9qmwRd8wwn7sF97DtYfK8WuNHtFalCAwyi 8LxJJYJCLWoMhZ+V8GZm+FOex5qkQAjnZrtNlbQJ8ro4r+rpKXtmMFFhfa+7L+PA Kom08eUK8skxAzfDDijZPh10VtJ66uBoiDPdT+uCBehcBIcmSTrKjFGX -----END CERTIFICATE----- "> foo 07070100000028000081A40000000000000000000000016357C4F700001BF0000000000000000000000000000000000000003400000000paranoia-0.2.1/internal/certificate/testdata/test-3- ----BEGIN CERTIFICATE ----- MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU 1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== -----END CERTIFICATE----- jslfskdfjskljf - - ---BEGIN CERTI FICATE----- MIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG EwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy bmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP VaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv h8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE ahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ EASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC DTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7 qwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g K4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI KwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n ZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB B QUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY /iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/ zG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza HFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto WHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6 yuGnBXj8ytqU0CwIPX4WecigUCAkVDNx -- - --END CERTIFICATE----- echo "-----BEGIN CERTIFICATE----- M IIEdjCCA16gAwIBAgIIcR5k4dkoe04wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMzEyMDkzODMwWhcNMTQwNjEwMDAwMDAw WjBoMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEXMBUGA1UEAwwOd3d3 Lmdvb2dsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4zYCe m0oUBhwE0EwBr65eBOcgcQO2PaSIAB2dEP/c1EMX2tOy0ov8rk83ePhJ+MWdT1z6 jge9X4zQQI8ZyA9qIiwrKBZOi8DNUvrqNZC7fJAVRrb9aX/99uYOJCypIbpmWG1q fhbHjJewhwf8xYPj71eU4rLG80a+DapWmphtfq3h52lDQIBzLVf1yYbyrTaELaz4 NXF7HXb5YkId/gxIsSzM0aFUVu2o8sJcLYAsJqwfFKBKOMxUcn545nlspf0mTcWZ 0APlbwsKznNs4/xCDwIxxWjjqgHrYAFl6y07i1gzbAOqdNEyR24p+3JWI8WZBlBI dk2KGj0W1fIfsvyxAgMBAAGjggFBMIIBPTAdBgNVHSUEFjAUBggrBgEFBQcDAQYI KwYBBQUHAwIwGQYDVR0RBBIwEIIOd3d3Lmdvb2dsZS5jb20waAYIKwYBBQUHAQEE XDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0 MCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0G A1UdDgQWBBTXD5Bx6iqT+dmEhbFL4OUoHyZn8zAMBgNVHRMBAf8EAjAAMB8GA1Ud IwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMBcGA1UdIAQQMA4wDAYKKwYBBAHW eQIFATAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lB RzIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCR3RJtHzgDh33b/MI1ugiki+nl8Ikj 5larbJRE/rcA5oite+QJyAr6SU1gJJ/ rRrK3ItVEHr9L621BCM7GSdoNMjB9MMcf t JAW0kYGJ+wqKm53wG/JaOADTnnq2Mt/j6F2uvjgN/ouns1nRHufIvd370N0LeH+ orKqTuAPzXK7imQk6+OycYABbqCtC/9qmwRd8wwn7sF97DtYfK8WuNHtFalCAwyi 8LxJJYJCLWoMhZ+V8GZm+FOex5qkQAjnZrtNlbQJ8ro4r+rpKXtmMFFhfa+7L+PA Kom08eUK8skxAzfDDijZPh10VtJ66uBoiDPdT+uCBehcBIcmSTrKjFGX -----END CERTIFICATE----- "> foo lksfjlsdf sdfsd fsdfsd fs fsdfsf - ----BEGIN CERTIFICATE ----- MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG fsdfsf - ----BEGIN CERTIFICATE ----- MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU 1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== -----END CERTIFICATE----- echo " - ----BEGIN CERTIFICATE ----- MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU 1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== -----END CERTIFICATE----- " > hello-world 07070100000029000081A40000000000000000000000016357C4F700000F6D000000000000000000000000000000000000003400000000paranoia-0.2.1/internal/certificate/testdata/test-4-----BEGIN CERTIFICATE----- MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU 1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG EwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy bmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB ahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ DTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7 VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g K4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI KwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n ZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB BQUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY /iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/ zG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza HFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto WHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6 yuGnBXj8ytqU0CwIPX4WecigUCAkVDNx -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEdjCCA16gAwIBAgIIcR5k4dkoe04wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMzEyMDkzODMwWhcNMTQwNjEwMDAwMDAw WjBoMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEXMBUGA1UEAwwOd3d3 Lmdvb2dsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4zYCe m0oUBhwE0EwBr65eBOcgcQO2PaSIAB2dEP/c1EMX2tOy0ov8rk83ePhJ+MWdT1z6 jge9X4zQQI8ZyA9qIiwrKBZOi8DNUvrqNZC7fJAVRrb9aX/99uYOJCypIbpmWG1q fhbHjJewhwf8xYPj71eU4rLG80a+DapWmphtfq3h52lDQIBzLVf1yYbyrTaELaz4 NXF7HXb5YkId/gxIsSzM0aFUVu2o8sJcLYAsJqwfFKBKOMxUcn545nlspf0mTcWZ 0APlbwsKznNs4/xCDwIxxWjjqgHrYAFl6y07i1gzbAOqdNEyR24p+3JWI8WZBlBI dk2KGj0W1fIfsvyxAgMBAAGjggFBMIIBPTAdBgNVHSUEFjAUBggrBgEFBQcDAQYI KwYBBQUHAwIwGQYDVR0RBBIwEIIOd3d3Lmdvb2dsZS5jb20waAYIKwYBBQUHAQEE XDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0 MCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0G A1UdDgQWBBTXD5Bx6iqT+dmEhbFL4OUoHyZn8zAMBgNVHRMBAf8EAjAAMB8GA1Ud IwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMBcGA1UdIAQQMA4wDAYKKwYBBAHW eQIFATAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lB RzIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCR3RJtHzgDh33b/MI1ugiki+nl8Ikj 5larbJRE/rcA5oite+QJyAr6SU1gJJ/rRrK3ItVEHr9L621BCM7GSdoNMjB9MMcf tJAW0kYGJ+wqKm53wG/JaOADTnnq2Mt/j6F2uvjgN/ouns1nRHufIvd370N0LeH+ orKqTuAPzXK7imQk6+OycYABbqCtC/9qmwRd8wwn7sF97DtYfK8WuNHtFalCAwyi 8LxJJYJCLWoMhZ+V8GZm+FOex5qkQAjnZrtNlbQJ8ro4r+rpKXtmMFFhfa+7L+PA Kom08eUK8skxAzfDDijZPh10VtJ66uBoiDPdT+uCBehcBIcmSTrKjFGX -----END CERTIFICATE----- 0707010000002A000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000001E00000000paranoia-0.2.1/internal/image0707010000002B000081A40000000000000000000000016357C4F7000007A4000000000000000000000000000000000000002700000000paranoia-0.2.1/internal/image/image.go// SPDX-License-Identifier: Apache-2.0 package image import ( "context" "fmt" "io" "os" "strings" "github.com/google/go-containerregistry/pkg/crane" crapi "github.com/google/go-containerregistry/pkg/v1" "github.com/pkg/errors" "github.com/jetstack/paranoia/internal/certificate" ) // FindImageCertificates will pull or load the image with the given name, scan // for X.509 certificates, and return the result. func FindImageCertificates(ctx context.Context, name string, opts ...Option) (*certificate.ParsedCertificates, error) { o := makeOptions(opts...) name = strings.TrimSpace(name) var ( img crapi.Image err error ) switch { case name == "-": var f *os.File f, err = os.CreateTemp(os.TempDir(), "paranoia-") if err != nil { return nil, fmt.Errorf("failed to create temporary file: %w", err) } defer os.RemoveAll(f.Name()) if _, err := io.Copy(f, os.Stdin); err != nil { return nil, fmt.Errorf("failed to write image to temporary file: %w", err) } if err := f.Close(); err != nil { return nil, fmt.Errorf("failed to close temporary file: %w", err) } img, err = crane.Load(f.Name(), o.craneOpts...) case strings.HasPrefix(name, "file://"): img, err = crane.Load(strings.TrimPrefix(name, "file://"), o.craneOpts...) default: img, err = crane.Pull(name, o.craneOpts...) } if err != nil { return nil, fmt.Errorf("failed to load image: %w", err) } var exportErr error exportDone := make(chan struct{}) r, w := io.Pipe() defer r.Close() defer w.Close() go func() { if err := crane.Export(img, w); err != nil { exportErr = err } close(exportDone) }() parsedCertificates, err := certificate.FindCertificates(context.TODO(), r) if err != nil { return nil, errors.Wrap(err, "failed to search for certificates in container image") } <-exportDone if exportErr != nil { return nil, errors.Wrap(err, "error when exporting image") } return parsedCertificates, nil } 0707010000002C000081A40000000000000000000000016357C4F700001B48000000000000000000000000000000000000002C00000000paranoia-0.2.1/internal/image/image_test.go// SPDX-License-Identifier: Apache-2.0 package image import ( "bytes" "context" "encoding/json" "fmt" "io/ioutil" "net/http/httptest" "net/url" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/registry" v1 "github.com/google/go-containerregistry/pkg/v1" "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/jetstack/paranoia/internal/certificate" ) func TestFindImageCertificates_Platform(t *testing.T) { host := setupRegistry(t) // Create a multi-arch image and push it to the registry idx := makeTestIndex( t, map[string]v1.Image{ "linux/amd64": makeTestImage( t, map[string]string{ "linux-amd64.crt": "testdata/linux-amd64", }, ), "linux/arm64": makeTestImage( t, map[string]string{ "linux-arm64.crt": "testdata/linux-arm64", }, ), }, ) idxTag := fmt.Sprintf("%s/%s:%s", host, "repo", "idx") idxRef, err := name.ParseReference(idxTag) if err != nil { t.Fatalf("unexpected error parsing reference: %s", err) } if err := remote.WriteIndex(idxRef, idx); err != nil { t.Fatalf("unexpected error writing index: %s", err) } // Push a lone image to the registry img := makeTestImage( t, map[string]string{ "image.crt": "testdata/image", }, ) imgTag := fmt.Sprintf("%s/%s:%s", host, "repo", "tag") imgRef, err := name.ParseReference(imgTag) if err != nil { t.Fatalf("unexpected error parsing reference: %s", err) } if err := remote.Write(imgRef, img); err != nil { t.Fatalf("unexpected error writing index: %s", err) } testCases := map[string]func(t *testing.T){ "default to linux/amd64 when no platform is set": func(t *testing.T) { gotCerts, err := FindImageCertificates(context.TODO(), idxTag) if err != nil { t.Fatalf("unexpected error finding certificates: %s", err) } wantCerts := &certificate.ParsedCertificates{ Found: []certificate.Found{ { Location: "/linux-amd64.crt", Parser: "pem", }, }, } if diff := cmp.Diff(wantCerts, gotCerts, cmpopts.IgnoreFields(certificate.Found{}, "Certificate", "FingerprintSha1", "FingerprintSha256")); diff != "" { t.Fatalf("unexpected certificates:\n%s", diff) } }, "return the correct image when linux/arm64 is set": func(t *testing.T) { platform, err := v1.ParsePlatform("linux/arm64") if err != nil { t.Fatalf("unexpected error parsing platform: %s", err) } gotCerts, err := FindImageCertificates(context.TODO(), idxTag, WithPlatform(platform)) if err != nil { t.Fatalf("unexpected error finding certificates: %s", err) } wantCerts := &certificate.ParsedCertificates{ Found: []certificate.Found{ { Location: "/linux-arm64.crt", Parser: "pem", }, }, } if diff := cmp.Diff(wantCerts, gotCerts, cmpopts.IgnoreFields(certificate.Found{}, "Certificate", "FingerprintSha1", "FingerprintSha256")); diff != "" { t.Fatalf("unexpected certificates:\n%s", diff) } }, "a platform that doesn't have a manifest in the index should return an error": func(t *testing.T) { platform, err := v1.ParsePlatform("linux/386") if err != nil { t.Fatalf("unexpected error parsing platform: %s", err) } if _, err := FindImageCertificates(context.TODO(), idxTag, WithPlatform(platform)); err == nil { t.Fatalf("expected error but got nil") } }, "the platform option should be ignored when the target is just an image": func(t *testing.T) { platform, err := v1.ParsePlatform("linux/arm64") if err != nil { t.Fatalf("unexpected error parsing platform: %s", err) } gotCerts, err := FindImageCertificates(context.TODO(), imgTag, WithPlatform(platform)) if err != nil { t.Fatalf("unexpected error finding certificates: %s", err) } wantCerts := &certificate.ParsedCertificates{ Found: []certificate.Found{ { Location: "/image.crt", Parser: "pem", }, }, } if diff := cmp.Diff(wantCerts, gotCerts, cmpopts.IgnoreFields(certificate.Found{}, "Certificate", "FingerprintSha1", "FingerprintSha256")); diff != "" { t.Fatalf("unexpected certificates:\n%s", diff) } }, } for n, fn := range testCases { t.Run(n, fn) } } func makeTestImage(t *testing.T, fileMap map[string]string) v1.Image { m := map[string][]byte{} for path, f := range fileMap { data, err := ioutil.ReadFile(f) if err != nil { t.Fatalf("unexpected error reading file: %s", err) } m[path] = data } img, err := crane.Image(m) if err != nil { t.Fatalf("unexpected error creating image: %s", err) } return img } func makeTestIndex(t *testing.T, imgs map[string]v1.Image) v1.ImageIndex { manifest := v1.IndexManifest{ SchemaVersion: 2, MediaType: types.OCIImageIndex, Manifests: []v1.Descriptor{}, } images := make(map[v1.Hash]v1.Image) for platform, img := range imgs { p, err := v1.ParsePlatform(platform) if err != nil { t.Fatalf("unexpected error parsing platform: %s", err) } rawManifest, err := img.RawManifest() if err != nil { t.Fatalf("unexpected error getting raw manifest: %s", err) } digest, size, err := v1.SHA256(bytes.NewReader(rawManifest)) if err != nil { t.Fatalf("unexpected error getting digest: %s", err) } mediaType, err := img.MediaType() if err != nil { t.Fatalf("unexpected error getting media type: %s", err) } manifest.Manifests = append(manifest.Manifests, v1.Descriptor{ Digest: digest, Size: size, MediaType: mediaType, Platform: p, }) images[digest] = img } return &testIndex{ images: images, manifest: &manifest, } } type testIndex struct { images map[v1.Hash]v1.Image manifest *v1.IndexManifest } func (i *testIndex) MediaType() (types.MediaType, error) { return i.manifest.MediaType, nil } func (i *testIndex) Digest() (v1.Hash, error) { return partial.Digest(i) } func (i *testIndex) Size() (int64, error) { return partial.Size(i) } func (i *testIndex) IndexManifest() (*v1.IndexManifest, error) { return i.manifest, nil } func (i *testIndex) RawManifest() ([]byte, error) { m, err := i.IndexManifest() if err != nil { return nil, err } return json.Marshal(m) } func (i *testIndex) Image(h v1.Hash) (v1.Image, error) { if img, ok := i.images[h]; ok { return img, nil } return nil, fmt.Errorf("image not found: %v", h) } func (i *testIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) { return nil, fmt.Errorf("image not found: %v", h) } func setupRegistry(t *testing.T) string { r := httptest.NewServer(registry.New()) t.Cleanup(r.Close) u, err := url.Parse(r.URL) if err != nil { t.Fatalf("unexpected error parsing registry url: %s", err) } return u.Host } 0707010000002D000081A40000000000000000000000016357C4F7000002A8000000000000000000000000000000000000002900000000paranoia-0.2.1/internal/image/options.gopackage image import ( "github.com/google/go-containerregistry/pkg/crane" v1 "github.com/google/go-containerregistry/pkg/v1" ) // Option is a functional option that configures image operations type Option func(*options) type options struct { craneOpts []crane.Option } func makeOptions(opts ...Option) *options { o := &options{} for _, opt := range opts { opt(o) } return o } // WithPlatform is a functional option that configures the platform (i.e // linux/amd64) images are resolved to. func WithPlatform(platform *v1.Platform) Option { return func(o *options) { if platform != nil { o.craneOpts = append(o.craneOpts, crane.WithPlatform(platform)) } } } 0707010000002E000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000002700000000paranoia-0.2.1/internal/image/testdata0707010000002F000081A40000000000000000000000016357C4F7000004C0000000000000000000000000000000000000002D00000000paranoia-0.2.1/internal/image/testdata/image-----BEGIN CERTIFICATE----- MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU 1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== -----END CERTIFICATE----- 07070100000030000081A40000000000000000000000016357C4F7000004C0000000000000000000000000000000000000003300000000paranoia-0.2.1/internal/image/testdata/linux-amd64-----BEGIN CERTIFICATE----- MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU 1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== -----END CERTIFICATE----- 07070100000031000081A40000000000000000000000016357C4F7000005AC000000000000000000000000000000000000003300000000paranoia-0.2.1/internal/image/testdata/linux-arm64-----BEGIN CERTIFICATE----- MIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG EwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy bmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP VaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv h8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE ahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ EASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC DTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7 qwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g K4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI KwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n ZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB BQUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY /iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/ zG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza HFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto WHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6 yuGnBXj8ytqU0CwIPX4WecigUCAkVDNx -----END CERTIFICATE----- 07070100000032000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000001F00000000paranoia-0.2.1/internal/output07070100000033000081A40000000000000000000000016357C4F700000310000000000000000000000000000000000000002700000000paranoia-0.2.1/internal/output/json.go// SPDX-License-Identifier: Apache-2.0 package output type JSONOutput struct { Certificates []JSONCertificate `json:"certificates"` PartialCertificates []JSONPartialCertificate `json:"partials,omitempty"` } type JSONCertificate struct { FileLocation string `json:"fileLocation"` Owner string `json:"owner"` Parser string `json:"parser"` Signature string `json:"signature"` NotBefore string `json:"notBefore"` NotAfter string `json:"notAfter"` FingerprintSHA1 string `json:"fingerprintSHA1"` FingerprintSHA256 string `json:"fingerprintSHA256"` } type JSONPartialCertificate struct { FileLocation string `json:"fileLocation"` Reason string `json:"reason"` Parser string `json:"parser"` } 07070100000034000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000001D00000000paranoia-0.2.1/internal/util07070100000035000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000002600000000paranoia-0.2.1/internal/util/checksum07070100000036000081A40000000000000000000000016357C4F700000347000000000000000000000000000000000000003200000000paranoia-0.2.1/internal/util/checksum/checksum.go// SPDX-License-Identifier: Apache-2.0 package checksum import ( "encoding/hex" "errors" ) func ParseSHA1(s string) ([20]byte, error) { b, err := hex.DecodeString(s) if err != nil { return [20]byte{}, err } if len(b) != 20 { return [20]byte{}, errors.New("incorrect length for SHA1") } var o [20]byte copy(o[:], b[:20]) return o, nil } func ParseSHA256(s string) ([32]byte, error) { b, err := hex.DecodeString(s) if err != nil { return [32]byte{}, err } if len(b) != 32 { return [32]byte{}, errors.New("incorrect length for SHA256") } var o [32]byte copy(o[:], b[:32]) return o, nil } func MustParseSHA1(s string) [20]byte { o, err := ParseSHA1(s) if err != nil { panic(err) } return o } func MustParseSHA256(s string) [32]byte { o, err := ParseSHA256(s) if err != nil { panic(err) } return o } 07070100000037000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000002100000000paranoia-0.2.1/internal/validate07070100000038000081A40000000000000000000000016357C4F70000081E000000000000000000000000000000000000002B00000000paranoia-0.2.1/internal/validate/config.go// SPDX-License-Identifier: Apache-2.0 package validate import ( "errors" "fmt" "io/ioutil" "os" "gopkg.in/yaml.v3" ) var ExpectedVersion = "1" type Config struct { Version string `json:"version"` Allow []CertificateEntry `json:"allow,omitempty"` Forbid []CertificateEntry `json:"forbid,omitempty"` Require []CertificateEntry `json:"require,omitempty"` } type CertificateEntry struct { Fingerprints CertificateFingerprints `json:"fingerprints"` Comment string `json:"comment,omitempty"` } type CertificateFingerprints struct { Sha1 string `json:"sha1,omitempty"` Sha256 string `json:"sha256,omitempty"` } func LoadConfig(fileName string) (*Config, error) { b, err := ioutil.ReadFile(fileName) if err != nil { return nil, err } var contents map[string]interface{} err = yaml.Unmarshal(b, &contents) if err != nil { return nil, err } if contents["version"].(string) != ExpectedVersion { return nil, errors.New("Unsupported config version, expected " + ExpectedVersion + ", found" + contents["version"].(string)) } var c Config err = yaml.Unmarshal(b, &c) return &c, err } func stderr(s string) { _, err := fmt.Fprintln(os.Stderr, s) if err != nil { panic(err) } } func IsConfigValid(config *Config) bool { isValid := true for _, list := range []struct { list []CertificateEntry name string }{ { list: config.Allow, name: "allow", }, { list: config.Forbid, name: "forbid", }, { list: config.Require, name: "require", }, } { for i, ce := range list.list { f := ce.Fingerprints if f.Sha1 == "" && f.Sha256 == "" { isValid = false stderr(fmt.Sprintf("Entry at position %d in %s list has no fingerprints. A fingerprint is required to identify the certificate.", i, list.name)) } else if f.Sha1 != "" && f.Sha256 != "" { isValid = false stderr(fmt.Sprintf("Entry at position %d in %s list has both SHA1 and SHA256 fingerprints. Only one type of fingerprint is permitted on a certificate.", i, list.name)) } } } return isValid } 07070100000039000081A40000000000000000000000016357C4F7000014C1000000000000000000000000000000000000002D00000000paranoia-0.2.1/internal/validate/validate.go// SPDX-License-Identifier: Apache-2.0 package validate import ( "fmt" "github.com/pkg/errors" "github.com/jetstack/paranoia/internal/certificate" "github.com/jetstack/paranoia/internal/util/checksum" ) type Validator struct { config Config permissiveMode bool allowSHA1 map[[20]byte]bool allowSHA256 map[[32]byte]bool forbidSHA1 map[[20]byte]CertificateEntry forbidSHA256 map[[32]byte]CertificateEntry required []CertificateEntry } func (v *Validator) DescribeConfig() string { s := fmt.Sprintf("%d allowed, %d forbidden, and %d required certificates", len(v.allowSHA1)+len(v.allowSHA256), len(v.forbidSHA1)+len(v.forbidSHA256), len(v.required)) if v.permissiveMode { s += ", in permissive mode" } else { s += ", in strict mode" } return s } func NewValidator(config Config, permissiveMode bool) (*Validator, error) { if !IsConfigValid(&config) { return nil, fmt.Errorf("invalid validator config") } v := Validator{ config: config, permissiveMode: permissiveMode, allowSHA1: make(map[[20]byte]bool), allowSHA256: make(map[[32]byte]bool), forbidSHA1: make(map[[20]byte]CertificateEntry), forbidSHA256: make(map[[32]byte]CertificateEntry), required: config.Require, } if !permissiveMode { for i, allowed := range config.Allow { if allowed.Fingerprints.Sha256 != "" { sha, err := checksum.ParseSHA256(allowed.Fingerprints.Sha256) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("entry at position %d in allow list had invalid SHA256", i)) } v.allowSHA256[sha] = true } else if allowed.Fingerprints.Sha1 != "" { sha, err := checksum.ParseSHA1(allowed.Fingerprints.Sha1) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("entry at position %d in allow list had invalid SHA1", i)) } v.allowSHA1[sha] = true } } for i, required := range config.Require { if required.Fingerprints.Sha256 != "" { sha, err := checksum.ParseSHA256(required.Fingerprints.Sha256) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("entry at position %d in require list had invalid SHA256", i)) } v.allowSHA256[sha] = true } else if required.Fingerprints.Sha1 != "" { sha, err := checksum.ParseSHA1(required.Fingerprints.Sha1) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("entry at position %d in require list had invalid SHA1", i)) } v.allowSHA1[sha] = true } } } for i, forbidden := range config.Forbid { if forbidden.Fingerprints.Sha256 != "" { sha, err := checksum.ParseSHA256(forbidden.Fingerprints.Sha256) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("entry at position %d in forbid list had invalid SHA256", i)) } v.forbidSHA256[sha] = forbidden } else if forbidden.Fingerprints.Sha1 != "" { sha, err := checksum.ParseSHA1(forbidden.Fingerprints.Sha1) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("entry at position %d in forbid list had invalid SHA1", i)) } v.forbidSHA1[sha] = forbidden } } return &v, nil } type ForbiddenCert struct { Certificate certificate.Found Entry CertificateEntry } type Result struct { NotAllowedCertificates []certificate.Found ForbiddenCertificates []ForbiddenCert RequiredButAbsent []CertificateEntry } func (r *Result) IsPass() bool { return r != nil && len(r.ForbiddenCertificates) == 0 && len(r.NotAllowedCertificates) == 0 && len(r.RequiredButAbsent) == 0 } func (v *Validator) Validate(founds []certificate.Found) (Result, error) { var result Result sha1checksums := make(map[[20]byte]bool) sha256checksums := make(map[[32]byte]bool) for _, cert := range founds { sha1checksums[cert.FingerprintSha1] = true sha256checksums[cert.FingerprintSha256] = true if !v.permissiveMode { if !v.IsAllowed(cert) { result.NotAllowedCertificates = append(result.NotAllowedCertificates, cert) } } if b, ce := v.IsForbidden(cert); b { result.ForbiddenCertificates = append(result.ForbiddenCertificates, ForbiddenCert{ Certificate: cert, Entry: *ce, }) } } // Check for missing required certificates for _, required := range v.required { if required.Fingerprints.Sha256 != "" { s, err := checksum.ParseSHA256(required.Fingerprints.Sha256) if err != nil { return Result{}, err } if _, ok := sha256checksums[s]; !ok { result.RequiredButAbsent = append(result.RequiredButAbsent, required) } } else if required.Fingerprints.Sha1 != "" { s, err := checksum.ParseSHA1(required.Fingerprints.Sha1) if err != nil { return Result{}, err } if _, ok := sha1checksums[s]; !ok { result.RequiredButAbsent = append(result.RequiredButAbsent, required) } } } return result, nil } func (v *Validator) IsAllowed(result certificate.Found) bool { if _, ok := v.allowSHA1[result.FingerprintSha1]; ok { return true } if _, ok := v.allowSHA256[result.FingerprintSha256]; ok { return true } return false } func (v *Validator) IsForbidden(result certificate.Found) (bool, *CertificateEntry) { if ce, ok := v.forbidSHA1[result.FingerprintSha1]; ok { return true, &ce } if ce, ok := v.forbidSHA256[result.FingerprintSha256]; ok { return true, &ce } return false, nil } 0707010000003A000081A40000000000000000000000016357C4F700001D20000000000000000000000000000000000000003200000000paranoia-0.2.1/internal/validate/validate_test.go// SPDX-License-Identifier: Apache-2.0 package validate import ( "crypto/sha1" "crypto/sha256" "strconv" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jetstack/paranoia/internal/certificate" "github.com/jetstack/paranoia/internal/util/checksum" ) func TestValidator(t *testing.T) { t.Run("Non-Permissive Allow List", func(t *testing.T) { allowedSHA1 := "4ae840b224dccf3af3ac0827be5f885eded18a17" allowedSHA256 := "01be162c36a6e26951a7ba4fbe6fba11dc7f4b9d589a072fc9d0183fc3386413" config := Config{ Allow: []CertificateEntry{ { Fingerprints: CertificateFingerprints{ Sha1: allowedSHA1, }, }, { Fingerprints: CertificateFingerprints{ Sha256: allowedSHA256, }, }, }, } validator, err := NewValidator(config, false) require.NoError(t, err) t.Run("No found certs is fine", func(t *testing.T) { r, err := validator.Validate(nil) assert.NoError(t, err) assert.Truef(t, r.IsPass(), "Validation reported as failed, expected pass") }) t.Run("Accepts permitted certificates with SHA1", func(t *testing.T) { r, err := validator.Validate([]certificate.Found{ { FingerprintSha1: checksum.MustParseSHA1(allowedSHA1), FingerprintSha256: anySHA256(), }, }) assert.NoError(t, err) assert.Truef(t, r.IsPass(), "Validation reported as failed, expected pass") }) t.Run("Accepts permitted certificates with SHA256", func(t *testing.T) { r, err := validator.Validate([]certificate.Found{ { FingerprintSha1: checksum.MustParseSHA1("673e582506961a8ebc133cb7890cee768501b84a"), FingerprintSha256: checksum.MustParseSHA256(allowedSHA256), }, }) assert.NoError(t, err) assert.Truef(t, r.IsPass(), "Validation reported as failed") }) t.Run("Rejects other certificates", func(t *testing.T) { certWeDontWant := certificate.Found{ FingerprintSha1: checksum.MustParseSHA1("4749c6f4aeb2e06f6b71129a9697219e97166db4"), FingerprintSha256: checksum.MustParseSHA256("edfa7caf7f1274d54bacec91e21a5b1a04a7b94bf197f5c92070b8de148d9b37"), } r, err := validator.Validate([]certificate.Found{certWeDontWant}) assert.NoError(t, err) assert.Falsef(t, r.IsPass(), "Validation reported as passed, when we expected it to fail") assert.Contains(t, r.NotAllowedCertificates, certWeDontWant) }) }) t.Run("Permissive Allow List", func(t *testing.T) { validator, err := NewValidator(Config{}, true) require.NoError(t, err) certWeWant := certificate.Found{ FingerprintSha1: checksum.MustParseSHA1("4749c6f4aeb2e06f6b71129a9697219e97166db4"), FingerprintSha256: checksum.MustParseSHA256("edfa7caf7f1274d54bacec91e21a5b1a04a7b94bf197f5c92070b8de148d9b37"), } r, err := validator.Validate([]certificate.Found{certWeWant}) assert.NoError(t, err) assert.Truef(t, r.IsPass(), "Validation reported failed, when we expected it to pass") }) t.Run("Forbid List", func(t *testing.T) { forbiddenSHA1 := "4ae840b224dccf3af3ac0827be5f885eded18a17" forbiddenSHA256 := "01be162c36a6e26951a7ba4fbe6fba11dc7f4b9d589a072fc9d0183fc3386413" config := Config{ Forbid: []CertificateEntry{ { Fingerprints: CertificateFingerprints{ Sha1: forbiddenSHA1, }, }, { Fingerprints: CertificateFingerprints{ Sha256: forbiddenSHA256, }, }, }, } validator, err := NewValidator(config, false) require.NoError(t, err) t.Run("Fails on forbidden SHA1", func(t *testing.T) { forbiddenCert := certificate.Found{ FingerprintSha1: checksum.MustParseSHA1(forbiddenSHA1), FingerprintSha256: checksum.MustParseSHA256("edfa7caf7f1274d54bacec91e21a5b1a04a7b94bf197f5c92070b8de148d9b37"), } r, err := validator.Validate([]certificate.Found{forbiddenCert}) assert.NoError(t, err) assert.Falsef(t, r.IsPass(), "Validation reported passed, when expected it to fail") assert.Contains(t, r.ForbiddenCertificates, ForbiddenCert{Certificate: forbiddenCert, Entry: config.Forbid[0]}) }) t.Run("Fails on forbidden SHA256", func(t *testing.T) { forbiddenCert := certificate.Found{ FingerprintSha1: checksum.MustParseSHA1("4749c6f4aeb2e06f6b71129a9697219e97166db4"), FingerprintSha256: checksum.MustParseSHA256(forbiddenSHA256), } r, err := validator.Validate([]certificate.Found{forbiddenCert}) assert.NoError(t, err) assert.Falsef(t, r.IsPass(), "Validation reported passed, when expected it to fail") assert.Contains(t, r.ForbiddenCertificates, ForbiddenCert{Certificate: forbiddenCert, Entry: config.Forbid[1]}) }) }) t.Run("Fails when allowed SHA1 and forbidden SHA256", func(t *testing.T) { forbiddenSHA1 := "4ae840b224dccf3af3ac0827be5f885eded18a17" forbiddenSHA256 := "01be162c36a6e26951a7ba4fbe6fba11dc7f4b9d589a072fc9d0183fc3386413" config := Config{ Allow: []CertificateEntry{ { Fingerprints: CertificateFingerprints{ Sha1: forbiddenSHA1, }, }, }, Forbid: []CertificateEntry{ { Fingerprints: CertificateFingerprints{ Sha256: forbiddenSHA256, }, }, }, } validator, err := NewValidator(config, false) require.NoError(t, err) forbiddenCert := certificate.Found{ FingerprintSha1: checksum.MustParseSHA1(forbiddenSHA1), FingerprintSha256: checksum.MustParseSHA256(forbiddenSHA256), } r, err := validator.Validate([]certificate.Found{forbiddenCert}) assert.NoError(t, err) assert.Falsef(t, r.IsPass(), "Validation reported passed, when expected it to fail") assert.Contains(t, r.ForbiddenCertificates, ForbiddenCert{Certificate: forbiddenCert, Entry: config.Forbid[0]}) }) t.Run("Require List", func(t *testing.T) { requiredSHA1 := "4ae840b224dccf3af3ac0827be5f885eded18a17" requiredSHA256 := "01be162c36a6e26951a7ba4fbe6fba11dc7f4b9d589a072fc9d0183fc3386413" config := Config{ Require: []CertificateEntry{ { Fingerprints: CertificateFingerprints{ Sha256: requiredSHA256, }, }, { Fingerprints: CertificateFingerprints{ Sha1: requiredSHA1, }, }, }, } validator, err := NewValidator(config, false) require.NoError(t, err) t.Run("All required certs found", func(t *testing.T) { foundCerts := []certificate.Found{ {FingerprintSha1: checksum.MustParseSHA1(requiredSHA1)}, {FingerprintSha256: checksum.MustParseSHA256(requiredSHA256)}, } r, err := validator.Validate(foundCerts) assert.NoError(t, err) assert.Truef(t, r.IsPass(), "Validation reported as failed, when we expected it to pass") }) t.Run("Missing required cert", func(t *testing.T) { foundCert := certificate.Found{ FingerprintSha1: anySHA1(), FingerprintSha256: anySHA256(), } r, err := validator.Validate([]certificate.Found{foundCert}) assert.NoError(t, err) assert.Falsef(t, r.IsPass(), "Validation reported as passed, when we expected it to fail") assert.Contains(t, r.RequiredButAbsent, CertificateEntry{Fingerprints: CertificateFingerprints{Sha1: requiredSHA1}}) assert.Contains(t, r.RequiredButAbsent, CertificateEntry{Fingerprints: CertificateFingerprints{Sha256: requiredSHA256}}) }) }) } func anySHA1() [20]byte { timestamp := time.Now().Unix() return sha1.Sum([]byte(strconv.FormatInt(timestamp, 10))) } func anySHA256() [32]byte { timestamp := time.Now().Unix() return sha256.Sum256([]byte(strconv.FormatInt(timestamp, 10))) } 0707010000003B000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000001200000000paranoia-0.2.1/it0707010000003C000081ED0000000000000000000000016357C4F7000003CF000000000000000000000000000000000000001C00000000paranoia-0.2.1/it/export.sh#!/usr/bin/env bash set -euxo pipefail root_dir="$(dirname "${BASH_SOURCE[0]}")/.." root_dir="$(realpath "${root_dir}")" container_tag="container_two_certs" docker build "${root_dir}/test/container-two-certs" -t "${container_tag}" docker save "${container_tag}" | paranoia export --output json - > /tmp/paranoia.json # The container-two-certs image contains the Let's Encrypt X1 root and the DigiCert Global root. Using the test command # and a bit of bash magic, we verfy that the JSON output has the correct SHA256 fingerprints in the correct places. It # could only have these fingerprints if it found the certs and exported them correctly. test "$(jq ".certificates[].fingerprintSHA256" -r /tmp/paranoia.json | sort | head -n1)" = "4348a0e9444c78cb265e058d5e8944b4d84f9662bd26db257f8934a443c70161" test "$(jq ".certificates[].fingerprintSHA256" -r /tmp/paranoia.json | sort | tail -n1)" = "96bcec06264976f37460779acf28c5a7cfe8a3c0aae11a8ffcee05c0bddf08c6" echo "Pass" 0707010000003D000081A40000000000000000000000016357C4F700000080000000000000000000000000000000000000001700000000paranoia-0.2.1/main.go// SPDX-License-Identifier: Apache-2.0 package main import "github.com/jetstack/paranoia/cmd" func main() { cmd.Execute() } 0707010000003E000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000001400000000paranoia-0.2.1/test0707010000003F000041ED0000000000000000000000026357C4F700000000000000000000000000000000000000000000002800000000paranoia-0.2.1/test/container-two-certs07070100000040000081A40000000000000000000000016357C4F7000000C3000000000000000000000000000000000000003300000000paranoia-0.2.1/test/container-two-certs/DockerfileFROM scratch ADD https://cacerts.digicert.com/DigiCertGlobalRootCA.crt.pem /etc/ssl/certs/DigiCertGlobalRootCA.crt ADD https://letsencrypt.org/certs/isrgrootx1.pem /etc/ssl/certs/isrgrootx1.crt 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!276 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor