summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 18:11:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 18:11:55 +0000
commit5edd1635e6e67adc18d05795cf10dee10a99811b (patch)
tree28f214bdb516789ceefe88dba26471e2cce84b7c
parentInitial commit. (diff)
downloadgolang-github-containers-ocicrypt-upstream.tar.xz
golang-github-containers-ocicrypt-upstream.zip
Adding upstream version 1.1.9.upstream/1.1.9upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.github/workflows/codeql.yml74
-rw-r--r--.github/workflows/go.yml38
-rw-r--r--.gitignore1
-rw-r--r--.golangci.yml35
-rw-r--r--ADOPTERS.md10
-rw-r--r--CODE-OF-CONDUCT.md3
-rw-r--r--LICENSE189
-rw-r--r--MAINTAINERS6
-rw-r--r--Makefile35
-rw-r--r--README.md50
-rw-r--r--SECURITY.md3
-rw-r--r--blockcipher/blockcipher.go161
-rw-r--r--blockcipher/blockcipher_aes_ctr.go193
-rw-r--r--blockcipher/blockcipher_aes_ctr_test.go234
-rw-r--r--blockcipher/blockcipher_test.go124
-rw-r--r--config/config.go114
-rw-r--r--config/constructors.go246
-rw-r--r--config/keyprovider-config/config.go80
-rw-r--r--config/pkcs11config/config.go123
-rw-r--r--crypto/pkcs11/common.go134
-rw-r--r--crypto/pkcs11/pkcs11helpers.go485
-rw-r--r--crypto/pkcs11/pkcs11helpers_nocgo.go30
-rw-r--r--crypto/pkcs11/pkcs11helpers_test.go229
-rw-r--r--crypto/pkcs11/utils.go115
-rw-r--r--docs/cex-ep11.md86
-rw-r--r--docs/keyprovider.md74
-rw-r--r--docs/pkcs11.md320
-rw-r--r--encryption.go356
-rw-r--r--encryption_test.go143
-rw-r--r--go.mod29
-rw-r--r--go.sum60
-rw-r--r--gpg.go432
-rw-r--r--gpgvault.go100
-rw-r--r--helpers/parse_helpers.go377
-rw-r--r--keywrap/jwe/keywrapper_jwe.go137
-rw-r--r--keywrap/jwe/keywrapper_jwe_test.go345
-rw-r--r--keywrap/keyprovider/keyprovider.go244
-rw-r--r--keywrap/keyprovider/keyprovider_test.go378
-rw-r--r--keywrap/keywrap.go48
-rw-r--r--keywrap/pgp/keywrapper_gpg.go272
-rw-r--r--keywrap/pgp/keywrapper_gpg_test.go190
-rw-r--r--keywrap/pgp/testingkeys_test.go178
-rw-r--r--keywrap/pkcs11/keywrapper_pkcs11.go152
-rw-r--r--keywrap/pkcs11/keywrapper_pkcs11_test.go217
-rw-r--r--keywrap/pkcs7/keywrapper_pkcs7.go137
-rw-r--r--keywrap/pkcs7/keywrapper_pkcs7_test.go254
-rw-r--r--reader.go40
-rwxr-xr-xscripts/softhsm_setup287
-rw-r--r--spec/spec.go20
-rw-r--r--utils/delayedreader.go109
-rw-r--r--utils/delayedreader_test.go72
-rw-r--r--utils/ioutils.go58
-rw-r--r--utils/keyprovider/keyprovider.pb.go243
-rw-r--r--utils/keyprovider/keyprovider.proto17
-rw-r--r--utils/softhsm/softhsm.go91
-rw-r--r--utils/testing.go165
-rw-r--r--utils/utils.go249
57 files changed, 8592 insertions, 0 deletions
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..e0a3df6
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,74 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ "main" ]
+ schedule:
+ - cron: '00 10 1 * *'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'go' ]
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+ # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+ # queries: security-extended,security-and-quality
+
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v2
+
+ # ℹī¸ Command-line programs to run using the OS shell.
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+ # If the Autobuild fails above, remove it and uncomment the following three lines.
+ # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+
+ # - run: |
+ # echo "Run, Build Application using script"
+ # ./location_of_script_within_repo/buildscript.sh
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
new file mode 100644
index 0000000..0036153
--- /dev/null
+++ b/.github/workflows/go.yml
@@ -0,0 +1,38 @@
+name: Go
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ go: [ '1.21', '1.20' ]
+ name: Go Version ${{ matrix.go }}
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install deps
+ run: sudo apt-get install -y gnutls-bin softhsm2
+
+ - name: Setup go
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ matrix.go }}
+
+ - name: Install
+ run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.1
+
+ - name: Build
+ run: make
+
+ - name: Run golangci-lint
+ run: make check
+
+ - name: Test
+ run: make test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b25c15b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*~
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..d3800d1
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,35 @@
+linters:
+ enable:
+ - depguard
+ - staticcheck
+ - unconvert
+ - gofmt
+ - goimports
+ - revive
+ - ineffassign
+ - vet
+ - unused
+ - misspell
+
+linters-settings:
+ depguard:
+ rules:
+ main:
+ files:
+ - $all
+ deny:
+ - pkg: "io/ioutil"
+
+ revive:
+ severity: error
+ rules:
+ - name: indent-error-flow
+ severity: warning
+ disabled: false
+
+ - name: error-strings
+ disabled: false
+
+ staticcheck:
+ # Suppress reports of deprecated packages
+ checks: ["-SA1019"]
diff --git a/ADOPTERS.md b/ADOPTERS.md
new file mode 100644
index 0000000..fa4b03b
--- /dev/null
+++ b/ADOPTERS.md
@@ -0,0 +1,10 @@
+Below are list of adopters of the `ocicrypt` library or supports use of OCI encrypted images:
+- [skopeo](https://github.com/containers/skopeo)
+- [buildah](https://github.com/containers/buildah)
+- [containerd](https://github.com/containerd/imgcrypt)
+- [nerdctl](https://github.com/containerd/nerdctl)
+- [distribution](https://github.com/distribution/distribution)
+
+Below are the list of projects that are in the process of adopting support:
+- [quay](https://github.com/quay/quay)
+- [kata-containers](https://github.com/kata-containers/kata-containers)
diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md
new file mode 100644
index 0000000..d68f8db
--- /dev/null
+++ b/CODE-OF-CONDUCT.md
@@ -0,0 +1,3 @@
+## The OCIcrypt Library Project Community Code of Conduct
+
+The OCIcrypt Library project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/main/CODE-OF-CONDUCT.md).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9535635
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,189 @@
+
+ Apache License
+ Version 2.0, January 2004
+ https://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
+
+ 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
+
+ https://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.
diff --git a/MAINTAINERS b/MAINTAINERS
new file mode 100644
index 0000000..af38d03
--- /dev/null
+++ b/MAINTAINERS
@@ -0,0 +1,6 @@
+# ocicrypt maintainers
+#
+# Github ID, Name, Email Address
+lumjjb, Brandon Lum, lumjjb@gmail.com
+stefanberger, Stefan Berger, stefanb@linux.ibm.com
+arronwy, Arron Wang, arron.wang@intel.com
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..97ddeef
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,35 @@
+# Copyright The containerd Authors.
+
+# 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.
+
+.PHONY: check build decoder generate-protobuf
+
+all: build
+
+FORCE:
+
+check:
+ golangci-lint run
+
+build: vendor
+ go build ./...
+
+vendor:
+ go mod tidy
+
+test:
+ go clean -testcache
+ go test ./... -test.v
+
+generate-protobuf:
+ protoc -I utils/keyprovider/ utils/keyprovider/keyprovider.proto --go_out=plugins=grpc:utils/keyprovider
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b69d14e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,50 @@
+# OCIcrypt Library
+
+The `ocicrypt` library is the OCI image spec implementation of container image encryption. More details of the spec can be seen in the [OCI repository](https://github.com/opencontainers/image-spec/pull/775). The purpose of this library is to encode spec structures and consts in code, as well as provide a consistent implementation of image encryption across container runtimes and build tools.
+
+Consumers of OCIcrypt:
+
+- [containerd/imgcrypt](https://github.com/containerd/imgcrypt)
+- [cri-o](https://github.com/cri-o/cri-o)
+- [skopeo](https://github.com/containers/skopeo)
+
+
+## Usage
+
+There are various levels of usage for this library. The main consumers of these would be runtime/build tools, and a more specific use would be in the ability to extend cryptographic function.
+
+### Runtime/Build tool usage
+
+The general exposed interface a runtime/build tool would use, would be to perform encryption or decryption of layers:
+
+```
+package "github.com/containers/ocicrypt"
+func EncryptLayer(ec *config.EncryptConfig, encOrPlainLayerReader io.Reader, desc ocispec.Descriptor) (io.Reader, EncryptLayerFinalizer, error)
+func DecryptLayer(dc *config.DecryptConfig, encLayerReader io.Reader, desc ocispec.Descriptor, unwrapOnly bool) (io.Reader, digest.Digest, error)
+```
+
+The settings/parameters to these functions can be specified via creation of an encryption config with the `github.com/containers/ocicrypt/config` package. We note that because setting of annotations and other fields of the layer descriptor is done through various means in different runtimes/build tools, it is the responsibility of the caller to still ensure that the layer descriptor follows the OCI specification (i.e. encoding, setting annotations, etc.).
+
+
+### Crypto Agility and Extensibility
+
+The implementation for both symmetric and asymmetric encryption used in this library are behind 2 main interfaces, which users can extend if need be. These are in the following packages:
+- github.com/containers/ocicrypt/blockcipher - LayerBlockCipher interface for block ciphers
+- github.com/containers/ocicrypt/keywrap - KeyWrapper interface for key wrapping
+
+We note that adding interfaces here is risky outside the OCI spec is not recommended, unless for very specialized and confined usecases. Please open an issue or PR if there is a general usecase that could be added to the OCI spec.
+
+
+#### Keyprovider interface
+
+As part of the keywrap interface, there is a [keyprovider](https://github.com/containers/ocicrypt/blob/main/docs/keyprovider.md) implementation that allows one to call out to a binary or service.
+
+
+## Security Issues
+
+We consider security issues related to this library critical. Please report and security related issues by emailing maintainers in the [MAINTAINERS](MAINTAINERS) file.
+
+
+## Ocicrypt Pkcs11 Support
+
+Ocicrypt Pkcs11 support is currently experiemental. For more details, please refer to the [this document](docs/pkcs11.md).
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..ea98cb1
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,3 @@
+## Security and Disclosure Information Policy for the OCIcrypt Library Project
+
+The OCIcrypt Library Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/main/SECURITY.md) for the Containers Projects.
diff --git a/blockcipher/blockcipher.go b/blockcipher/blockcipher.go
new file mode 100644
index 0000000..0c485d5
--- /dev/null
+++ b/blockcipher/blockcipher.go
@@ -0,0 +1,161 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package blockcipher
+
+import (
+ "errors"
+ "fmt"
+ "io"
+
+ "github.com/opencontainers/go-digest"
+)
+
+// LayerCipherType is the ciphertype as specified in the layer metadata
+type LayerCipherType string
+
+// TODO: Should be obtained from OCI spec once included
+const (
+ AES256CTR LayerCipherType = "AES_256_CTR_HMAC_SHA256"
+)
+
+// PrivateLayerBlockCipherOptions includes the information required to encrypt/decrypt
+// an image which are sensitive and should not be in plaintext
+type PrivateLayerBlockCipherOptions struct {
+ // SymmetricKey represents the symmetric key used for encryption/decryption
+ // This field should be populated by Encrypt/Decrypt calls
+ SymmetricKey []byte `json:"symkey"`
+
+ // Digest is the digest of the original data for verification.
+ // This is NOT populated by Encrypt/Decrypt calls
+ Digest digest.Digest `json:"digest"`
+
+ // CipherOptions contains the cipher metadata used for encryption/decryption
+ // This field should be populated by Encrypt/Decrypt calls
+ CipherOptions map[string][]byte `json:"cipheroptions"`
+}
+
+// PublicLayerBlockCipherOptions includes the information required to encrypt/decrypt
+// an image which are public and can be deduplicated in plaintext across multiple
+// recipients
+type PublicLayerBlockCipherOptions struct {
+ // CipherType denotes the cipher type according to the list of OCI suppported
+ // cipher types.
+ CipherType LayerCipherType `json:"cipher"`
+
+ // Hmac contains the hmac string to help verify encryption
+ Hmac []byte `json:"hmac"`
+
+ // CipherOptions contains the cipher metadata used for encryption/decryption
+ // This field should be populated by Encrypt/Decrypt calls
+ CipherOptions map[string][]byte `json:"cipheroptions"`
+}
+
+// LayerBlockCipherOptions contains the public and private LayerBlockCipherOptions
+// required to encrypt/decrypt an image
+type LayerBlockCipherOptions struct {
+ Public PublicLayerBlockCipherOptions
+ Private PrivateLayerBlockCipherOptions
+}
+
+// LayerBlockCipher returns a provider for encrypt/decrypt functionality
+// for handling the layer data for a specific algorithm
+type LayerBlockCipher interface {
+ // GenerateKey creates a symmetric key
+ GenerateKey() ([]byte, error)
+ // Encrypt takes in layer data and returns the ciphertext and relevant LayerBlockCipherOptions
+ Encrypt(layerDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, Finalizer, error)
+ // Decrypt takes in layer ciphertext data and returns the plaintext and relevant LayerBlockCipherOptions
+ Decrypt(layerDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, LayerBlockCipherOptions, error)
+}
+
+// LayerBlockCipherHandler is the handler for encrypt/decrypt for layers
+type LayerBlockCipherHandler struct {
+ cipherMap map[LayerCipherType]LayerBlockCipher
+}
+
+// Finalizer is called after data blobs are written, and returns the LayerBlockCipherOptions for the encrypted blob
+type Finalizer func() (LayerBlockCipherOptions, error)
+
+// GetOpt returns the value of the cipher option and if the option exists
+func (lbco LayerBlockCipherOptions) GetOpt(key string) (value []byte, ok bool) {
+ if v, ok := lbco.Public.CipherOptions[key]; ok {
+ return v, ok
+ } else if v, ok := lbco.Private.CipherOptions[key]; ok {
+ return v, ok
+ } else {
+ return nil, false
+ }
+}
+
+func wrapFinalizerWithType(fin Finalizer, typ LayerCipherType) Finalizer {
+ return func() (LayerBlockCipherOptions, error) {
+ lbco, err := fin()
+ if err != nil {
+ return LayerBlockCipherOptions{}, err
+ }
+ lbco.Public.CipherType = typ
+ return lbco, err
+ }
+}
+
+// Encrypt is the handler for the layer decryption routine
+func (h *LayerBlockCipherHandler) Encrypt(plainDataReader io.Reader, typ LayerCipherType) (io.Reader, Finalizer, error) {
+ if c, ok := h.cipherMap[typ]; ok {
+ sk, err := c.GenerateKey()
+ if err != nil {
+ return nil, nil, err
+ }
+ opt := LayerBlockCipherOptions{
+ Private: PrivateLayerBlockCipherOptions{
+ SymmetricKey: sk,
+ },
+ }
+ encDataReader, fin, err := c.Encrypt(plainDataReader, opt)
+ if err == nil {
+ fin = wrapFinalizerWithType(fin, typ)
+ }
+ return encDataReader, fin, err
+ }
+ return nil, nil, fmt.Errorf("unsupported cipher type: %s", typ)
+}
+
+// Decrypt is the handler for the layer decryption routine
+func (h *LayerBlockCipherHandler) Decrypt(encDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, LayerBlockCipherOptions, error) {
+ typ := opt.Public.CipherType
+ if typ == "" {
+ return nil, LayerBlockCipherOptions{}, errors.New("no cipher type provided")
+ }
+ if c, ok := h.cipherMap[typ]; ok {
+ return c.Decrypt(encDataReader, opt)
+ }
+ return nil, LayerBlockCipherOptions{}, fmt.Errorf("unsupported cipher type: %s", typ)
+}
+
+// NewLayerBlockCipherHandler returns a new default handler
+func NewLayerBlockCipherHandler() (*LayerBlockCipherHandler, error) {
+ h := LayerBlockCipherHandler{
+ cipherMap: map[LayerCipherType]LayerBlockCipher{},
+ }
+
+ var err error
+ h.cipherMap[AES256CTR], err = NewAESCTRLayerBlockCipher(256)
+ if err != nil {
+ return nil, fmt.Errorf("unable to set up Cipher AES-256-CTR: %w", err)
+ }
+
+ return &h, nil
+}
diff --git a/blockcipher/blockcipher_aes_ctr.go b/blockcipher/blockcipher_aes_ctr.go
new file mode 100644
index 0000000..7db03e2
--- /dev/null
+++ b/blockcipher/blockcipher_aes_ctr.go
@@ -0,0 +1,193 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package blockcipher
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hmac"
+ "crypto/rand"
+ "crypto/sha256"
+ "errors"
+ "fmt"
+ "hash"
+ "io"
+
+ "github.com/containers/ocicrypt/utils"
+)
+
+// AESCTRLayerBlockCipher implements the AES CTR stream cipher
+type AESCTRLayerBlockCipher struct {
+ keylen int // in bytes
+ reader io.Reader
+ encrypt bool
+ stream cipher.Stream
+ err error
+ hmac hash.Hash
+ expHmac []byte
+ doneEncrypting bool
+}
+
+type aesctrcryptor struct {
+ bc *AESCTRLayerBlockCipher
+}
+
+// NewAESCTRLayerBlockCipher returns a new AES SIV block cipher of 256 or 512 bits
+func NewAESCTRLayerBlockCipher(bits int) (LayerBlockCipher, error) {
+ if bits != 256 {
+ return nil, errors.New("AES CTR bit count not supported")
+ }
+ return &AESCTRLayerBlockCipher{keylen: bits / 8}, nil
+}
+
+func (r *aesctrcryptor) Read(p []byte) (int, error) {
+ var (
+ o int
+ )
+
+ if r.bc.err != nil {
+ return 0, r.bc.err
+ }
+
+ o, err := utils.FillBuffer(r.bc.reader, p)
+ if err != nil {
+ if err == io.EOF {
+ r.bc.err = err
+ } else {
+ return 0, err
+ }
+ }
+
+ if !r.bc.encrypt {
+ if _, err := r.bc.hmac.Write(p[:o]); err != nil {
+ r.bc.err = fmt.Errorf("could not write to hmac: %w", err)
+ return 0, r.bc.err
+ }
+
+ if r.bc.err == io.EOF {
+ // Before we return EOF we let the HMAC comparison
+ // provide a verdict
+ if !hmac.Equal(r.bc.hmac.Sum(nil), r.bc.expHmac) {
+ r.bc.err = fmt.Errorf("could not properly decrypt byte stream; exp hmac: '%x', actual hmac: '%s'", r.bc.expHmac, r.bc.hmac.Sum(nil))
+ return 0, r.bc.err
+ }
+ }
+ }
+
+ r.bc.stream.XORKeyStream(p[:o], p[:o])
+
+ if r.bc.encrypt {
+ if _, err := r.bc.hmac.Write(p[:o]); err != nil {
+ r.bc.err = fmt.Errorf("could not write to hmac: %w", err)
+ return 0, r.bc.err
+ }
+
+ if r.bc.err == io.EOF {
+ // Final data encrypted; Do the 'then-MAC' part
+ r.bc.doneEncrypting = true
+ }
+ }
+
+ return o, r.bc.err
+}
+
+// init initializes an instance
+func (bc *AESCTRLayerBlockCipher) init(encrypt bool, reader io.Reader, opts LayerBlockCipherOptions) (LayerBlockCipherOptions, error) {
+ var (
+ err error
+ )
+
+ key := opts.Private.SymmetricKey
+ if len(key) != bc.keylen {
+ return LayerBlockCipherOptions{}, fmt.Errorf("invalid key length of %d bytes; need %d bytes", len(key), bc.keylen)
+ }
+
+ nonce, ok := opts.GetOpt("nonce")
+ if !ok {
+ nonce = make([]byte, aes.BlockSize)
+ if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
+ return LayerBlockCipherOptions{}, fmt.Errorf("unable to generate random nonce: %w", err)
+ }
+ }
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return LayerBlockCipherOptions{}, fmt.Errorf("aes.NewCipher failed: %w", err)
+ }
+
+ bc.reader = reader
+ bc.encrypt = encrypt
+ bc.stream = cipher.NewCTR(block, nonce)
+ bc.err = nil
+ bc.hmac = hmac.New(sha256.New, key)
+ bc.expHmac = opts.Public.Hmac
+ bc.doneEncrypting = false
+
+ if !encrypt && len(bc.expHmac) == 0 {
+ return LayerBlockCipherOptions{}, errors.New("HMAC is not provided for decryption process")
+ }
+
+ lbco := LayerBlockCipherOptions{
+ Private: PrivateLayerBlockCipherOptions{
+ SymmetricKey: key,
+ CipherOptions: map[string][]byte{
+ "nonce": nonce,
+ },
+ },
+ }
+
+ return lbco, nil
+}
+
+// GenerateKey creates a synmmetric key
+func (bc *AESCTRLayerBlockCipher) GenerateKey() ([]byte, error) {
+ key := make([]byte, bc.keylen)
+ if _, err := io.ReadFull(rand.Reader, key); err != nil {
+ return nil, err
+ }
+ return key, nil
+}
+
+// Encrypt takes in layer data and returns the ciphertext and relevant LayerBlockCipherOptions
+func (bc *AESCTRLayerBlockCipher) Encrypt(plainDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, Finalizer, error) {
+ lbco, err := bc.init(true, plainDataReader, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ finalizer := func() (LayerBlockCipherOptions, error) {
+ if !bc.doneEncrypting {
+ return LayerBlockCipherOptions{}, errors.New("Read()ing not complete, unable to finalize")
+ }
+ if lbco.Public.CipherOptions == nil {
+ lbco.Public.CipherOptions = map[string][]byte{}
+ }
+ lbco.Public.Hmac = bc.hmac.Sum(nil)
+ return lbco, nil
+ }
+ return &aesctrcryptor{bc}, finalizer, nil
+}
+
+// Decrypt takes in layer ciphertext data and returns the plaintext and relevant LayerBlockCipherOptions
+func (bc *AESCTRLayerBlockCipher) Decrypt(encDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, LayerBlockCipherOptions, error) {
+ lbco, err := bc.init(false, encDataReader, opt)
+ if err != nil {
+ return nil, LayerBlockCipherOptions{}, err
+ }
+
+ return utils.NewDelayedReader(&aesctrcryptor{bc}, 1024*10), lbco, nil
+}
diff --git a/blockcipher/blockcipher_aes_ctr_test.go b/blockcipher/blockcipher_aes_ctr_test.go
new file mode 100644
index 0000000..0e19eeb
--- /dev/null
+++ b/blockcipher/blockcipher_aes_ctr_test.go
@@ -0,0 +1,234 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package blockcipher
+
+import (
+ "bytes"
+ _ "crypto/sha256"
+ "io"
+ "testing"
+)
+
+func TestBlockCipherAesCtrCreateValid(t *testing.T) {
+ _, err := NewAESCTRLayerBlockCipher(256)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestBlockCipherAesCtrCreateInvalid(t *testing.T) {
+ _, err := NewAESCTRLayerBlockCipher(8)
+ if err == nil {
+ t.Fatal("Test should have failed due to invalid cipher size")
+ }
+ _, err = NewAESCTRLayerBlockCipher(255)
+ if err == nil {
+ t.Fatal("Test should have failed due to invalid cipher size")
+ }
+}
+
+func TestBlockCipherAesCtrEncryption(t *testing.T) {
+ var (
+ symKey = []byte("01234567890123456789012345678912")
+ opt = LayerBlockCipherOptions{
+ Private: PrivateLayerBlockCipherOptions{
+ SymmetricKey: symKey,
+ },
+ }
+ layerData = []byte("this is some data")
+ )
+
+ bc, err := NewAESCTRLayerBlockCipher(256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ layerDataReader := bytes.NewReader(layerData)
+ ciphertextReader, finalizer, err := bc.Encrypt(layerDataReader, opt)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Use a different instantiated object to indicate an invocation at a diff time
+ bc2, err := NewAESCTRLayerBlockCipher(256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ciphertext := make([]byte, 1024)
+ encsize, err := ciphertextReader.Read(ciphertext)
+ if err != io.EOF {
+ t.Fatal("Expected EOF")
+ }
+
+ ciphertextTestReader := bytes.NewReader(ciphertext[:encsize])
+
+ lbco, err := finalizer()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ plaintextReader, _, err := bc2.Decrypt(ciphertextTestReader, lbco)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ plaintext := make([]byte, 1024)
+ size, err := plaintextReader.Read(plaintext)
+ if err != io.EOF {
+ t.Fatal("Expected EOF")
+ }
+
+ if string(plaintext[:size]) != string(layerData) {
+ t.Fatalf("expected %q, got %q", layerData, plaintext[:size])
+ }
+}
+
+func TestBlockCipherAesCtrEncryptionInvalidKey(t *testing.T) {
+ var (
+ symKey = []byte("01234567890123456789012345678912")
+ opt = LayerBlockCipherOptions{
+ Private: PrivateLayerBlockCipherOptions{
+ SymmetricKey: symKey,
+ },
+ }
+ layerData = []byte("this is some data")
+ )
+
+ bc, err := NewAESCTRLayerBlockCipher(256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ layerDataReader := bytes.NewReader(layerData)
+
+ ciphertextReader, finalizer, err := bc.Encrypt(layerDataReader, opt)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Use a different instantiated object to indicate an invokation at a diff time
+ bc2, err := NewAESCTRLayerBlockCipher(256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ciphertext := make([]byte, 1024)
+ encsize, err := ciphertextReader.Read(ciphertext)
+ if err != io.EOF {
+ t.Fatal("Expected EOF")
+ }
+ ciphertextTestReader := bytes.NewReader(ciphertext[:encsize])
+
+ lbco, err := finalizer()
+ if err != nil {
+ t.Fatal(err)
+ }
+ lbco.Private.SymmetricKey = []byte("aaa34567890123456789012345678912")
+
+ plaintextReader, _, err := bc2.Decrypt(ciphertextTestReader, lbco)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ plaintext := make([]byte, 1024)
+ // first time read may not hit EOF of original source
+ _, _ = plaintextReader.Read(plaintext)
+ // now we must have hit eof and evaluated the plaintext
+ _, err = plaintextReader.Read(plaintext)
+ if err == nil {
+ t.Fatal("Read() should have failed due to wrong key")
+ }
+}
+
+func TestBlockCipherAesCtrEncryptionInvalidKeyLength(t *testing.T) {
+ var (
+ symKey = []byte("012345")
+ opt = LayerBlockCipherOptions{
+ Private: PrivateLayerBlockCipherOptions{
+ SymmetricKey: symKey,
+ },
+ }
+ layerData = []byte("this is some data")
+ )
+
+ bc, err := NewAESCTRLayerBlockCipher(256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ layerDataReader := bytes.NewReader(layerData)
+ _, _, err = bc.Encrypt(layerDataReader, opt)
+ if err == nil {
+ t.Fatal("Test should have failed due to invalid key length")
+ }
+}
+
+func TestBlockCipherAesCtrEncryptionInvalidHMAC(t *testing.T) {
+ var (
+ symKey = []byte("01234567890123456789012345678912")
+ opt = LayerBlockCipherOptions{
+ Private: PrivateLayerBlockCipherOptions{
+ SymmetricKey: symKey,
+ },
+ }
+ layerData = []byte("this is some data")
+ )
+
+ bc, err := NewAESCTRLayerBlockCipher(256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ layerDataReader := bytes.NewReader(layerData)
+
+ ciphertextReader, finalizer, err := bc.Encrypt(layerDataReader, opt)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Use a different instantiated object to indicate an invokation at a diff time
+ bc2, err := NewAESCTRLayerBlockCipher(256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ciphertext := make([]byte, 1024)
+ encsize, err := ciphertextReader.Read(ciphertext)
+ if err != io.EOF {
+ t.Fatal("Expected EOF")
+ }
+ ciphertextTestReader := bytes.NewReader(ciphertext[:encsize])
+
+ lbco, err := finalizer()
+ if err != nil {
+ t.Fatal(err)
+ }
+ lbco.Public.Hmac = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
+
+ plaintextReader, _, err := bc2.Decrypt(ciphertextTestReader, lbco)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ plaintext := make([]byte, 1024)
+ // we will hit the error the first time
+ _, err = plaintextReader.Read(plaintext)
+ if err == nil || err == io.EOF {
+ t.Fatal("Read() should have failed due to Invalid HMAC verification")
+ }
+}
diff --git a/blockcipher/blockcipher_test.go b/blockcipher/blockcipher_test.go
new file mode 100644
index 0000000..3c0d459
--- /dev/null
+++ b/blockcipher/blockcipher_test.go
@@ -0,0 +1,124 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package blockcipher
+
+import (
+ "bytes"
+ "io"
+ "testing"
+)
+
+func TestBlockCipherHandlerCreate(t *testing.T) {
+ _, err := NewLayerBlockCipherHandler()
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestBlockCipherEncryption(t *testing.T) {
+ var (
+ layerData = []byte("this is some data")
+ )
+
+ h, err := NewLayerBlockCipherHandler()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ layerDataReader := bytes.NewReader(layerData)
+
+ ciphertextReader, finalizer, err := h.Encrypt(layerDataReader, AES256CTR)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ciphertext := make([]byte, 1024)
+ encsize, err := ciphertextReader.Read(ciphertext)
+ if err != io.EOF {
+ t.Fatal("Expected EOF")
+ }
+
+ ciphertextTestReader := bytes.NewReader(ciphertext[:encsize])
+ lbco, err := finalizer()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Use a different instantiated object to indicate an invokation at a diff time
+ plaintextReader, _, err := h.Decrypt(ciphertextTestReader, lbco)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ plaintext := make([]byte, 1024)
+ decsize, err := plaintextReader.Read(plaintext)
+ if err != nil && err != io.EOF {
+ t.Fatal("Reading the plaintext should not have failed")
+ }
+
+ if string(plaintext[:decsize]) != string(layerData) {
+ t.Fatal("Decrypted data is incorrect")
+ }
+}
+
+func TestBlockCipherEncryptionInvalidKey(t *testing.T) {
+ var (
+ layerData = []byte("this is some data")
+ )
+
+ h, err := NewLayerBlockCipherHandler()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ layerDataReader := bytes.NewReader(layerData)
+
+ ciphertextReader, finalizer, err := h.Encrypt(layerDataReader, AES256CTR)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Use a different instantiated object to indicate an invokation at a diff time
+ bc2, err := NewAESCTRLayerBlockCipher(256)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ciphertext := make([]byte, 1024)
+ encsize, err := ciphertextReader.Read(ciphertext)
+ if err != io.EOF {
+ t.Fatal("Expected EOF")
+ }
+
+ ciphertextTestReader := bytes.NewReader(ciphertext[:encsize])
+ lbco, err := finalizer()
+ if err != nil {
+ t.Fatal(err)
+ }
+ lbco.Private.SymmetricKey = []byte("aaa34567890123456789012345678901")
+
+ plaintextReader, _, err := bc2.Decrypt(ciphertextTestReader, lbco)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ plaintext := make([]byte, 1024)
+ _, err = plaintextReader.Read(plaintext)
+ if err == nil || err == io.EOF {
+ t.Fatal("Read() should have failed due to wrong key")
+ }
+}
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..d960766
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,114 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package config
+
+// EncryptConfig is the container image PGP encryption configuration holding
+// the identifiers of those that will be able to decrypt the container and
+// the PGP public keyring file data that contains their public keys.
+type EncryptConfig struct {
+ // map holding 'gpg-recipients', 'gpg-pubkeyringfile', 'pubkeys', 'x509s'
+ Parameters map[string][][]byte
+
+ DecryptConfig DecryptConfig
+}
+
+// DecryptConfig wraps the Parameters map that holds the decryption key
+type DecryptConfig struct {
+ // map holding 'privkeys', 'x509s', 'gpg-privatekeys'
+ Parameters map[string][][]byte
+}
+
+// CryptoConfig is a common wrapper for EncryptConfig and DecrypConfig that can
+// be passed through functions that share much code for encryption and decryption
+type CryptoConfig struct {
+ EncryptConfig *EncryptConfig
+ DecryptConfig *DecryptConfig
+}
+
+// InitDecryption initialized a CryptoConfig object with parameters used for decryption
+func InitDecryption(dcparameters map[string][][]byte) CryptoConfig {
+ return CryptoConfig{
+ DecryptConfig: &DecryptConfig{
+ Parameters: dcparameters,
+ },
+ }
+}
+
+// InitEncryption initializes a CryptoConfig object with parameters used for encryption
+// It also takes dcparameters that may be needed for decryption when adding a recipient
+// to an already encrypted image
+func InitEncryption(parameters, dcparameters map[string][][]byte) CryptoConfig {
+ return CryptoConfig{
+ EncryptConfig: &EncryptConfig{
+ Parameters: parameters,
+ DecryptConfig: DecryptConfig{
+ Parameters: dcparameters,
+ },
+ },
+ }
+}
+
+// CombineCryptoConfigs takes a CryptoConfig list and creates a single CryptoConfig
+// containing the crypto configuration of all the key bundles
+func CombineCryptoConfigs(ccs []CryptoConfig) CryptoConfig {
+ ecparam := map[string][][]byte{}
+ ecdcparam := map[string][][]byte{}
+ dcparam := map[string][][]byte{}
+
+ for _, cc := range ccs {
+ if ec := cc.EncryptConfig; ec != nil {
+ addToMap(ecparam, ec.Parameters)
+ addToMap(ecdcparam, ec.DecryptConfig.Parameters)
+ }
+
+ if dc := cc.DecryptConfig; dc != nil {
+ addToMap(dcparam, dc.Parameters)
+ }
+ }
+
+ return CryptoConfig{
+ EncryptConfig: &EncryptConfig{
+ Parameters: ecparam,
+ DecryptConfig: DecryptConfig{
+ Parameters: ecdcparam,
+ },
+ },
+ DecryptConfig: &DecryptConfig{
+ Parameters: dcparam,
+ },
+ }
+
+}
+
+// AttachDecryptConfig adds DecryptConfig to the field of EncryptConfig so that
+// the decryption parameters can be used to add recipients to an existing image
+// if the user is able to decrypt it.
+func (ec *EncryptConfig) AttachDecryptConfig(dc *DecryptConfig) {
+ if dc != nil {
+ addToMap(ec.DecryptConfig.Parameters, dc.Parameters)
+ }
+}
+
+func addToMap(orig map[string][][]byte, add map[string][][]byte) {
+ for k, v := range add {
+ if ov, ok := orig[k]; ok {
+ orig[k] = append(ov, v...)
+ } else {
+ orig[k] = v
+ }
+ }
+}
diff --git a/config/constructors.go b/config/constructors.go
new file mode 100644
index 0000000..f7f29cd
--- /dev/null
+++ b/config/constructors.go
@@ -0,0 +1,246 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package config
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/containers/ocicrypt/crypto/pkcs11"
+ "gopkg.in/yaml.v3"
+)
+
+// EncryptWithJwe returns a CryptoConfig to encrypt with jwe public keys
+func EncryptWithJwe(pubKeys [][]byte) (CryptoConfig, error) {
+ dc := DecryptConfig{}
+ ep := map[string][][]byte{
+ "pubkeys": pubKeys,
+ }
+
+ return CryptoConfig{
+ EncryptConfig: &EncryptConfig{
+ Parameters: ep,
+ DecryptConfig: dc,
+ },
+ DecryptConfig: &dc,
+ }, nil
+}
+
+// EncryptWithPkcs7 returns a CryptoConfig to encrypt with pkcs7 x509 certs
+func EncryptWithPkcs7(x509s [][]byte) (CryptoConfig, error) {
+ dc := DecryptConfig{}
+
+ ep := map[string][][]byte{
+ "x509s": x509s,
+ }
+
+ return CryptoConfig{
+ EncryptConfig: &EncryptConfig{
+ Parameters: ep,
+ DecryptConfig: dc,
+ },
+ DecryptConfig: &dc,
+ }, nil
+}
+
+// EncryptWithGpg returns a CryptoConfig to encrypt with configured gpg parameters
+func EncryptWithGpg(gpgRecipients [][]byte, gpgPubRingFile []byte) (CryptoConfig, error) {
+ dc := DecryptConfig{}
+ ep := map[string][][]byte{
+ "gpg-recipients": gpgRecipients,
+ "gpg-pubkeyringfile": {gpgPubRingFile},
+ }
+
+ return CryptoConfig{
+ EncryptConfig: &EncryptConfig{
+ Parameters: ep,
+ DecryptConfig: dc,
+ },
+ DecryptConfig: &dc,
+ }, nil
+}
+
+// EncryptWithPkcs11 returns a CryptoConfig to encrypt with configured pkcs11 parameters
+func EncryptWithPkcs11(pkcs11Config *pkcs11.Pkcs11Config, pkcs11Pubkeys, pkcs11Yamls [][]byte) (CryptoConfig, error) {
+ dc := DecryptConfig{}
+ ep := map[string][][]byte{}
+
+ if len(pkcs11Yamls) > 0 {
+ if pkcs11Config == nil {
+ return CryptoConfig{}, errors.New("pkcs11Config must not be nil")
+ }
+ p11confYaml, err := yaml.Marshal(pkcs11Config)
+ if err != nil {
+ return CryptoConfig{}, fmt.Errorf("Could not marshal Pkcs11Config to Yaml: %w", err)
+ }
+
+ dc = DecryptConfig{
+ Parameters: map[string][][]byte{
+ "pkcs11-config": {p11confYaml},
+ },
+ }
+ ep["pkcs11-yamls"] = pkcs11Yamls
+ }
+ if len(pkcs11Pubkeys) > 0 {
+ ep["pkcs11-pubkeys"] = pkcs11Pubkeys
+ }
+
+ return CryptoConfig{
+ EncryptConfig: &EncryptConfig{
+ Parameters: ep,
+ DecryptConfig: dc,
+ },
+ DecryptConfig: &dc,
+ }, nil
+}
+
+// EncryptWithKeyProvider returns a CryptoConfig to encrypt with configured keyprovider parameters
+func EncryptWithKeyProvider(keyProviders [][]byte) (CryptoConfig, error) {
+ dc := DecryptConfig{}
+ ep := make(map[string][][]byte)
+ for _, keyProvider := range keyProviders {
+ keyProvidersStr := string(keyProvider)
+ idx := strings.Index(keyProvidersStr, ":")
+ if idx > 0 {
+ ep[keyProvidersStr[:idx]] = append(ep[keyProvidersStr[:idx]], []byte(keyProvidersStr[idx+1:]))
+ } else {
+ ep[keyProvidersStr] = append(ep[keyProvidersStr], []byte("Enabled"))
+ }
+ }
+
+ return CryptoConfig{
+ EncryptConfig: &EncryptConfig{
+ Parameters: ep,
+ DecryptConfig: dc,
+ },
+ DecryptConfig: &dc,
+ }, nil
+}
+
+// DecryptWithKeyProvider returns a CryptoConfig to decrypt with configured keyprovider parameters
+func DecryptWithKeyProvider(keyProviders [][]byte) (CryptoConfig, error) {
+ dp := make(map[string][][]byte)
+ ep := map[string][][]byte{}
+ for _, keyProvider := range keyProviders {
+ keyProvidersStr := string(keyProvider)
+ idx := strings.Index(keyProvidersStr, ":")
+ if idx > 0 {
+ dp[keyProvidersStr[:idx]] = append(dp[keyProvidersStr[:idx]], []byte(keyProvidersStr[idx+1:]))
+ } else {
+ dp[keyProvidersStr] = append(dp[keyProvidersStr], []byte("Enabled"))
+ }
+ }
+ dc := DecryptConfig{
+ Parameters: dp,
+ }
+ return CryptoConfig{
+ EncryptConfig: &EncryptConfig{
+ Parameters: ep,
+ DecryptConfig: dc,
+ },
+ DecryptConfig: &dc,
+ }, nil
+}
+
+// DecryptWithPrivKeys returns a CryptoConfig to decrypt with configured private keys
+func DecryptWithPrivKeys(privKeys [][]byte, privKeysPasswords [][]byte) (CryptoConfig, error) {
+ if len(privKeys) != len(privKeysPasswords) {
+ return CryptoConfig{}, errors.New("Length of privKeys should match length of privKeysPasswords")
+ }
+
+ dc := DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": privKeys,
+ "privkeys-passwords": privKeysPasswords,
+ },
+ }
+
+ ep := map[string][][]byte{}
+
+ return CryptoConfig{
+ EncryptConfig: &EncryptConfig{
+ Parameters: ep,
+ DecryptConfig: dc,
+ },
+ DecryptConfig: &dc,
+ }, nil
+}
+
+// DecryptWithX509s returns a CryptoConfig to decrypt with configured x509 certs
+func DecryptWithX509s(x509s [][]byte) (CryptoConfig, error) {
+ dc := DecryptConfig{
+ Parameters: map[string][][]byte{
+ "x509s": x509s,
+ },
+ }
+
+ ep := map[string][][]byte{}
+
+ return CryptoConfig{
+ EncryptConfig: &EncryptConfig{
+ Parameters: ep,
+ DecryptConfig: dc,
+ },
+ DecryptConfig: &dc,
+ }, nil
+}
+
+// DecryptWithGpgPrivKeys returns a CryptoConfig to decrypt with configured gpg private keys
+func DecryptWithGpgPrivKeys(gpgPrivKeys, gpgPrivKeysPwds [][]byte) (CryptoConfig, error) {
+ dc := DecryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-privatekeys": gpgPrivKeys,
+ "gpg-privatekeys-passwords": gpgPrivKeysPwds,
+ },
+ }
+
+ ep := map[string][][]byte{}
+
+ return CryptoConfig{
+ EncryptConfig: &EncryptConfig{
+ Parameters: ep,
+ DecryptConfig: dc,
+ },
+ DecryptConfig: &dc,
+ }, nil
+}
+
+// DecryptWithPkcs11Yaml returns a CryptoConfig to decrypt with pkcs11 YAML formatted key files
+func DecryptWithPkcs11Yaml(pkcs11Config *pkcs11.Pkcs11Config, pkcs11Yamls [][]byte) (CryptoConfig, error) {
+ p11confYaml, err := yaml.Marshal(pkcs11Config)
+ if err != nil {
+ return CryptoConfig{}, fmt.Errorf("Could not marshal Pkcs11Config to Yaml: %w", err)
+ }
+
+ dc := DecryptConfig{
+ Parameters: map[string][][]byte{
+ "pkcs11-yamls": pkcs11Yamls,
+ "pkcs11-config": {p11confYaml},
+ },
+ }
+
+ ep := map[string][][]byte{}
+
+ return CryptoConfig{
+ EncryptConfig: &EncryptConfig{
+ Parameters: ep,
+ DecryptConfig: dc,
+ },
+ DecryptConfig: &dc,
+ }, nil
+}
diff --git a/config/keyprovider-config/config.go b/config/keyprovider-config/config.go
new file mode 100644
index 0000000..4785a83
--- /dev/null
+++ b/config/keyprovider-config/config.go
@@ -0,0 +1,80 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package config
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+)
+
+// Command describes the structure of command, it consist of path and args, where path defines the location of
+// binary executable and args are passed on to the binary executable
+type Command struct {
+ Path string `json:"path,omitempty"`
+ Args []string `json:"args,omitempty"`
+}
+
+// KeyProviderAttrs describes the structure of key provider, it defines the way of invocation to key provider
+type KeyProviderAttrs struct {
+ Command *Command `json:"cmd,omitempty"`
+ Grpc string `json:"grpc,omitempty"`
+}
+
+// OcicryptConfig represents the format of an ocicrypt_provider.conf config file
+type OcicryptConfig struct {
+ KeyProviderConfig map[string]KeyProviderAttrs `json:"key-providers"`
+}
+
+const ENVVARNAME = "OCICRYPT_KEYPROVIDER_CONFIG"
+
+// parseConfigFile parses a configuration file; it is not an error if the configuration file does
+// not exist, so no error is returned.
+func parseConfigFile(filename string) (*OcicryptConfig, error) {
+ // a non-existent config file is not an error
+ _, err := os.Stat(filename)
+ if os.IsNotExist(err) {
+ return nil, nil
+ }
+
+ data, err := os.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+
+ ic := &OcicryptConfig{}
+ err = json.Unmarshal(data, ic)
+ return ic, err
+}
+
+// getConfiguration tries to read the configuration file at the following locations
+// ${OCICRYPT_KEYPROVIDER_CONFIG} == "/etc/ocicrypt_keyprovider.yaml"
+// If no configuration file could be found or read a null pointer is returned
+func GetConfiguration() (*OcicryptConfig, error) {
+ var ic *OcicryptConfig
+ var err error
+ filename := os.Getenv(ENVVARNAME)
+ if len(filename) > 0 {
+ ic, err = parseConfigFile(filename)
+ if err != nil {
+ return nil, fmt.Errorf("Error while parsing keyprovider config file: %w", err)
+ }
+ } else {
+ return nil, nil
+ }
+ return ic, nil
+}
diff --git a/config/pkcs11config/config.go b/config/pkcs11config/config.go
new file mode 100644
index 0000000..b4f0e4d
--- /dev/null
+++ b/config/pkcs11config/config.go
@@ -0,0 +1,123 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package pkcs11config
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path"
+
+ "github.com/containers/ocicrypt/crypto/pkcs11"
+ "gopkg.in/yaml.v3"
+)
+
+// OcicryptConfig represents the format of an imgcrypt.conf config file
+type OcicryptConfig struct {
+ Pkcs11Config pkcs11.Pkcs11Config `yaml:"pkcs11"`
+}
+
+const CONFIGFILE = "ocicrypt.conf"
+const ENVVARNAME = "OCICRYPT_CONFIG"
+
+// parseConfigFile parses a configuration file; it is not an error if the configuration file does
+// not exist, so no error is returned.
+// A config file may look like this:
+// module-directories:
+// - /usr/lib64/pkcs11/
+// - /usr/lib/pkcs11/
+// allowed-module-paths:
+// - /usr/lib64/pkcs11/
+// - /usr/lib/pkcs11/
+func parseConfigFile(filename string) (*OcicryptConfig, error) {
+ // a non-existent config file is not an error
+ _, err := os.Stat(filename)
+ if os.IsNotExist(err) {
+ return nil, nil
+ }
+
+ data, err := os.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+
+ ic := &OcicryptConfig{}
+ err = yaml.Unmarshal(data, ic)
+ return ic, err
+}
+
+// getConfiguration tries to read the configuration file at the following locations
+// 1) ${OCICRYPT_CONFIG} == "internal": use internal default allow-all policy
+// 2) ${OCICRYPT_CONFIG}
+// 3) ${XDG_CONFIG_HOME}/ocicrypt-pkcs11.conf
+// 4) ${HOME}/.config/ocicrypt-pkcs11.conf
+// 5) /etc/ocicrypt-pkcs11.conf
+// If no configuration file could be found or read a null pointer is returned
+func getConfiguration() (*OcicryptConfig, error) {
+ filename := os.Getenv(ENVVARNAME)
+ if len(filename) > 0 {
+ if filename == "internal" {
+ return getDefaultCryptoConfigOpts()
+ }
+ ic, err := parseConfigFile(filename)
+ if err != nil || ic != nil {
+ return ic, err
+ }
+ }
+ envvar := os.Getenv("XDG_CONFIG_HOME")
+ if len(envvar) > 0 {
+ ic, err := parseConfigFile(path.Join(envvar, CONFIGFILE))
+ if err != nil || ic != nil {
+ return ic, err
+ }
+ }
+ envvar = os.Getenv("HOME")
+ if len(envvar) > 0 {
+ ic, err := parseConfigFile(path.Join(envvar, ".config", CONFIGFILE))
+ if err != nil || ic != nil {
+ return ic, err
+ }
+ }
+ return parseConfigFile(path.Join("etc", CONFIGFILE))
+}
+
+// getDefaultCryptoConfigOpts returns default crypto config opts needed for pkcs11 module access
+func getDefaultCryptoConfigOpts() (*OcicryptConfig, error) {
+ mdyaml := pkcs11.GetDefaultModuleDirectoriesYaml("")
+ config := fmt.Sprintf("module-directories:\n"+
+ "%s"+
+ "allowed-module-paths:\n"+
+ "%s", mdyaml, mdyaml)
+ p11conf, err := pkcs11.ParsePkcs11ConfigFile([]byte(config))
+ return &OcicryptConfig{
+ Pkcs11Config: *p11conf,
+ }, err
+}
+
+// GetUserPkcs11Config gets the user's Pkcs11Conig either from a configuration file or if none is
+// found the default ones are returned
+func GetUserPkcs11Config() (*pkcs11.Pkcs11Config, error) {
+ fmt.Print("Note: pkcs11 support is currently experimental\n")
+ ic, err := getConfiguration()
+ if err != nil {
+ return &pkcs11.Pkcs11Config{}, err
+ }
+ if ic == nil {
+ return &pkcs11.Pkcs11Config{}, errors.New("No ocicrypt config file was found")
+ }
+ return &ic.Pkcs11Config, nil
+}
diff --git a/crypto/pkcs11/common.go b/crypto/pkcs11/common.go
new file mode 100644
index 0000000..072d7fe
--- /dev/null
+++ b/crypto/pkcs11/common.go
@@ -0,0 +1,134 @@
+/*
+ Copyright The ocicrypt Authors.
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package pkcs11
+
+import (
+ "fmt"
+
+ pkcs11uri "github.com/stefanberger/go-pkcs11uri"
+ "gopkg.in/yaml.v3"
+)
+
+// Pkcs11KeyFile describes the format of the pkcs11 (private) key file.
+// It also carries pkcs11 module related environment variables that are transferred to the
+// Pkcs11URI object and activated when the pkcs11 module is used.
+type Pkcs11KeyFile struct {
+ Pkcs11 struct {
+ Uri string `yaml:"uri"`
+ } `yaml:"pkcs11"`
+ Module struct {
+ Env map[string]string `yaml:"env,omitempty"`
+ } `yaml:"module"`
+}
+
+// Pkcs11KeyFileObject is a representation of the Pkcs11KeyFile with the pkcs11 URI as an object
+type Pkcs11KeyFileObject struct {
+ Uri *pkcs11uri.Pkcs11URI
+}
+
+// ParsePkcs11Uri parses a pkcs11 URI
+func ParsePkcs11Uri(uri string) (*pkcs11uri.Pkcs11URI, error) {
+ p11uri := pkcs11uri.New()
+ err := p11uri.Parse(uri)
+ if err != nil {
+ return nil, fmt.Errorf("Could not parse Pkcs11URI from file: %w", err)
+ }
+ return p11uri, err
+}
+
+// ParsePkcs11KeyFile parses a pkcs11 key file holding a pkcs11 URI describing a private key.
+// The file has the following yaml format:
+// pkcs11:
+// - uri : <pkcs11 uri>
+// An error is returned if the pkcs11 URI is malformed
+func ParsePkcs11KeyFile(yamlstr []byte) (*Pkcs11KeyFileObject, error) {
+ p11keyfile := Pkcs11KeyFile{}
+
+ err := yaml.Unmarshal(yamlstr, &p11keyfile)
+ if err != nil {
+ return nil, fmt.Errorf("Could not unmarshal pkcs11 keyfile: %w", err)
+ }
+
+ p11uri, err := ParsePkcs11Uri(p11keyfile.Pkcs11.Uri)
+ if err != nil {
+ return nil, err
+ }
+ p11uri.SetEnvMap(p11keyfile.Module.Env)
+
+ return &Pkcs11KeyFileObject{Uri: p11uri}, err
+}
+
+// IsPkcs11PrivateKey checks whether the given YAML represents a Pkcs11 private key
+func IsPkcs11PrivateKey(yamlstr []byte) bool {
+ _, err := ParsePkcs11KeyFile(yamlstr)
+ return err == nil
+}
+
+// IsPkcs11PublicKey checks whether the given YAML represents a Pkcs11 public key
+func IsPkcs11PublicKey(yamlstr []byte) bool {
+ _, err := ParsePkcs11KeyFile(yamlstr)
+ return err == nil
+}
+
+// Pkcs11Config describes the layout of a pkcs11 config file
+// The file has the following yaml format:
+// module-directories:
+// - /usr/lib64/pkcs11/
+// allowd-module-paths
+// - /usr/lib64/pkcs11/libsofthsm2.so
+type Pkcs11Config struct {
+ ModuleDirectories []string `yaml:"module-directories"`
+ AllowedModulePaths []string `yaml:"allowed-module-paths"`
+}
+
+// GetDefaultModuleDirectories returns module directories covering
+// a variety of Linux distros
+func GetDefaultModuleDirectories() []string {
+ dirs := []string{
+ "/usr/lib64/pkcs11/", // Fedora,RHEL,openSUSE
+ "/usr/lib/pkcs11/", // Fedora,ArchLinux
+ "/usr/local/lib/pkcs11/",
+ "/usr/lib/softhsm/", // Debian,Ubuntu
+ }
+
+ // Debian directory: /usr/lib/(x86_64|aarch64|arm|powerpc64le|riscv64|s390x)-linux-gnu/
+ hosttype, ostype, q := getHostAndOsType()
+ if len(hosttype) > 0 {
+ dir := fmt.Sprintf("/usr/lib/%s-%s-%s/", hosttype, ostype, q)
+ dirs = append(dirs, dir)
+ }
+ return dirs
+}
+
+// GetDefaultModuleDirectoresFormatted returns the default module directories formatted for YAML
+func GetDefaultModuleDirectoriesYaml(indent string) string {
+ res := ""
+
+ for _, dir := range GetDefaultModuleDirectories() {
+ res += indent + "- " + dir + "\n"
+ }
+ return res
+}
+
+// ParsePkcs11ConfigFile parses a pkcs11 config file hat influences the module search behavior
+// as well as the set of modules that users are allowed to use
+func ParsePkcs11ConfigFile(yamlstr []byte) (*Pkcs11Config, error) {
+ p11conf := Pkcs11Config{}
+
+ err := yaml.Unmarshal(yamlstr, &p11conf)
+ if err != nil {
+ return &p11conf, fmt.Errorf("Could not parse Pkcs11Config: %w", err)
+ }
+ return &p11conf, nil
+}
diff --git a/crypto/pkcs11/pkcs11helpers.go b/crypto/pkcs11/pkcs11helpers.go
new file mode 100644
index 0000000..fe047a1
--- /dev/null
+++ b/crypto/pkcs11/pkcs11helpers.go
@@ -0,0 +1,485 @@
+//go:build cgo
+// +build cgo
+
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package pkcs11
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha1"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "hash"
+ "net/url"
+ "os"
+ "strconv"
+ "strings"
+
+ "github.com/miekg/pkcs11"
+ pkcs11uri "github.com/stefanberger/go-pkcs11uri"
+)
+
+var (
+ // OAEPLabel defines the label we use for OAEP encryption; this cannot be changed
+ OAEPLabel = []byte("")
+
+ // OAEPSha1Params describes the OAEP parameters with sha1 hash algorithm; needed by SoftHSM
+ OAEPSha1Params = &pkcs11.OAEPParams{
+ HashAlg: pkcs11.CKM_SHA_1,
+ MGF: pkcs11.CKG_MGF1_SHA1,
+ SourceType: pkcs11.CKZ_DATA_SPECIFIED,
+ SourceData: OAEPLabel,
+ }
+ // OAEPSha256Params describes the OAEP parameters with sha256 hash algorithm
+ OAEPSha256Params = &pkcs11.OAEPParams{
+ HashAlg: pkcs11.CKM_SHA256,
+ MGF: pkcs11.CKG_MGF1_SHA256,
+ SourceType: pkcs11.CKZ_DATA_SPECIFIED,
+ SourceData: OAEPLabel,
+ }
+)
+
+// rsaPublicEncryptOAEP encrypts the given plaintext with the given *rsa.PublicKey; the
+// environment variable OCICRYPT_OAEP_HASHALG can be set to 'sha1' to force usage of sha1 for OAEP (SoftHSM).
+// This function is needed by clients who are using a public key file for pkcs11 encryption
+func rsaPublicEncryptOAEP(pubKey *rsa.PublicKey, plaintext []byte) ([]byte, string, error) {
+ var (
+ hashfunc hash.Hash
+ hashalg string
+ )
+
+ oaephash := os.Getenv("OCICRYPT_OAEP_HASHALG")
+ // The default is sha256 (previously was sha1)
+ switch strings.ToLower(oaephash) {
+ case "sha1":
+ hashfunc = sha1.New()
+ hashalg = "sha1"
+ case "sha256", "":
+ hashfunc = sha256.New()
+ hashalg = "sha256"
+ default:
+ return nil, "", fmt.Errorf("Unsupported OAEP hash '%s'", oaephash)
+ }
+ ciphertext, err := rsa.EncryptOAEP(hashfunc, rand.Reader, pubKey, plaintext, OAEPLabel)
+ if err != nil {
+ return nil, "", fmt.Errorf("rss.EncryptOAEP failed: %w", err)
+ }
+
+ return ciphertext, hashalg, nil
+}
+
+// pkcs11UriGetLoginParameters gets the parameters necessary for login from the Pkcs11URI
+// PIN and module are mandatory; slot-id is optional and if not found -1 will be returned
+// For a privateKeyOperation a PIN is required and if none is given, this function will return an error
+func pkcs11UriGetLoginParameters(p11uri *pkcs11uri.Pkcs11URI, privateKeyOperation bool) (string, string, int64, error) {
+ var (
+ pin string
+ err error
+ )
+ if privateKeyOperation {
+ if !p11uri.HasPIN() {
+ return "", "", 0, errors.New("Missing PIN for private key operation")
+ }
+ }
+ // some devices require a PIN to find a *public* key object, others don't
+ pin, _ = p11uri.GetPIN()
+
+ module, err := p11uri.GetModule()
+ if err != nil {
+ return "", "", 0, fmt.Errorf("No module available in pkcs11 URI: %w", err)
+ }
+
+ slotid := int64(-1)
+
+ slot, ok := p11uri.GetPathAttribute("slot-id", false)
+ if ok {
+ slotid, err = strconv.ParseInt(slot, 10, 64)
+ if err != nil {
+ return "", "", 0, fmt.Errorf("slot-id is not a valid number: %w", err)
+ }
+ if slotid < 0 {
+ return "", "", 0, fmt.Errorf("slot-id is a negative number")
+ }
+ if uint64(slotid) > 0xffffffff {
+ return "", "", 0, fmt.Errorf("slot-id is larger than 32 bit")
+ }
+ }
+
+ return pin, module, slotid, nil
+}
+
+// pkcs11UriGetKeyIdAndLabel gets the key label by retrieving the value of the 'object' attribute
+func pkcs11UriGetKeyIdAndLabel(p11uri *pkcs11uri.Pkcs11URI) (string, string, error) {
+ keyid, ok2 := p11uri.GetPathAttribute("id", false)
+ label, ok1 := p11uri.GetPathAttribute("object", false)
+ if !ok1 && !ok2 {
+ return "", "", errors.New("Neither 'id' nor 'object' attributes were found in pkcs11 URI")
+ }
+ return keyid, label, nil
+}
+
+// pkcs11OpenSession opens a session with a pkcs11 device at the given slot and logs in with the given PIN
+func pkcs11OpenSession(p11ctx *pkcs11.Ctx, slotid uint, pin string) (session pkcs11.SessionHandle, err error) {
+ session, err = p11ctx.OpenSession(slotid, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION)
+ if err != nil {
+ return 0, fmt.Errorf("OpenSession to slot %d failed: %w", slotid, err)
+ }
+ if len(pin) > 0 {
+ err = p11ctx.Login(session, pkcs11.CKU_USER, pin)
+ if err != nil {
+ _ = p11ctx.CloseSession(session)
+ return 0, fmt.Errorf("Could not login to device: %w", err)
+ }
+ }
+ return session, nil
+}
+
+// pkcs11UriLogin uses the given pkcs11 URI to select the pkcs11 module (shared library) and to get
+// the PIN to use for login; if the URI contains a slot-id, the given slot-id will be used, otherwise
+// one slot after the other will be attempted and the first one where login succeeds will be used
+func pkcs11UriLogin(p11uri *pkcs11uri.Pkcs11URI, privateKeyOperation bool) (ctx *pkcs11.Ctx, session pkcs11.SessionHandle, err error) {
+ pin, module, slotid, err := pkcs11UriGetLoginParameters(p11uri, privateKeyOperation)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ p11ctx := pkcs11.New(module)
+ if p11ctx == nil {
+ return nil, 0, errors.New("Please check module path, input is: " + module)
+ }
+
+ err = p11ctx.Initialize()
+ if err != nil {
+ p11Err := err.(pkcs11.Error)
+ if p11Err != pkcs11.CKR_CRYPTOKI_ALREADY_INITIALIZED {
+ return nil, 0, fmt.Errorf("Initialize failed: %w", err)
+ }
+ }
+
+ if slotid >= 0 {
+ session, err := pkcs11OpenSession(p11ctx, uint(slotid), pin)
+ return p11ctx, session, err
+ }
+
+ slots, err := p11ctx.GetSlotList(true)
+ if err != nil {
+ return nil, 0, fmt.Errorf("GetSlotList failed: %w", err)
+ }
+
+ tokenlabel, ok := p11uri.GetPathAttribute("token", false)
+ if !ok {
+ return nil, 0, errors.New("Missing 'token' attribute since 'slot-id' was not given")
+ }
+
+ for _, slot := range slots {
+ ti, err := p11ctx.GetTokenInfo(slot)
+ if err != nil || ti.Label != tokenlabel {
+ continue
+ }
+
+ session, err = pkcs11OpenSession(p11ctx, slot, pin)
+ if err == nil {
+ return p11ctx, session, err
+ }
+ }
+ if len(pin) > 0 {
+ return nil, 0, errors.New("Could not create session to any slot and/or log in")
+ }
+ return nil, 0, errors.New("Could not create session to any slot")
+}
+
+func pkcs11Logout(ctx *pkcs11.Ctx, session pkcs11.SessionHandle) {
+ _ = ctx.Logout(session)
+ _ = ctx.CloseSession(session)
+ _ = ctx.Finalize()
+ ctx.Destroy()
+}
+
+// findObject finds an object of the given class with the given keyid and/or label
+func findObject(p11ctx *pkcs11.Ctx, session pkcs11.SessionHandle, class uint, keyid, label string) (pkcs11.ObjectHandle, error) {
+ msg := ""
+
+ template := []*pkcs11.Attribute{
+ pkcs11.NewAttribute(pkcs11.CKA_CLASS, class),
+ }
+ if len(label) > 0 {
+ template = append(template, pkcs11.NewAttribute(pkcs11.CKA_LABEL, label))
+ msg = fmt.Sprintf("label '%s'", label)
+ }
+ if len(keyid) > 0 {
+ template = append(template, pkcs11.NewAttribute(pkcs11.CKA_ID, keyid))
+ if len(msg) > 0 {
+ msg += " and "
+ }
+ msg += url.PathEscape(keyid)
+ }
+
+ if err := p11ctx.FindObjectsInit(session, template); err != nil {
+ return 0, fmt.Errorf("FindObjectsInit failed: %w", err)
+ }
+
+ obj, _, err := p11ctx.FindObjects(session, 100)
+ if err != nil {
+ return 0, fmt.Errorf("FindObjects failed: %w", err)
+ }
+
+ if err := p11ctx.FindObjectsFinal(session); err != nil {
+ return 0, fmt.Errorf("FindObjectsFinal failed: %w", err)
+ }
+ if len(obj) > 1 {
+ return 0, fmt.Errorf("There are too many (=%d) keys with %s", len(obj), msg)
+ } else if len(obj) == 1 {
+ return obj[0], nil
+ }
+
+ return 0, fmt.Errorf("Could not find any object with %s", msg)
+}
+
+// publicEncryptOAEP uses a public key described by a pkcs11 URI to OAEP encrypt the given plaintext
+func publicEncryptOAEP(pubKey *Pkcs11KeyFileObject, plaintext []byte) ([]byte, string, error) {
+ oldenv, err := setEnvVars(pubKey.Uri.GetEnvMap())
+ if err != nil {
+ return nil, "", err
+ }
+ defer restoreEnv(oldenv)
+
+ p11ctx, session, err := pkcs11UriLogin(pubKey.Uri, false)
+ if err != nil {
+ return nil, "", err
+ }
+ defer pkcs11Logout(p11ctx, session)
+
+ keyid, label, err := pkcs11UriGetKeyIdAndLabel(pubKey.Uri)
+ if err != nil {
+ return nil, "", err
+ }
+
+ p11PubKey, err := findObject(p11ctx, session, pkcs11.CKO_PUBLIC_KEY, keyid, label)
+ if err != nil {
+ return nil, "", err
+ }
+
+ var hashalg string
+
+ var oaep *pkcs11.OAEPParams
+ oaephash := os.Getenv("OCICRYPT_OAEP_HASHALG")
+ // The default is sha256 (previously was sha1)
+ switch strings.ToLower(oaephash) {
+ case "sha1":
+ oaep = OAEPSha1Params
+ hashalg = "sha1"
+ case "sha256", "":
+ oaep = OAEPSha256Params
+ hashalg = "sha256"
+ default:
+ return nil, "", fmt.Errorf("Unsupported OAEP hash '%s'", oaephash)
+ }
+
+ err = p11ctx.EncryptInit(session, []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS_OAEP, oaep)}, p11PubKey)
+ if err != nil {
+ return nil, "", fmt.Errorf("EncryptInit error: %w", err)
+ }
+
+ ciphertext, err := p11ctx.Encrypt(session, plaintext)
+ if err != nil {
+ return nil, "", fmt.Errorf("Encrypt failed: %w", err)
+ }
+ return ciphertext, hashalg, nil
+}
+
+// privateDecryptOAEP uses a pkcs11 URI describing a private key to OAEP decrypt a ciphertext
+func privateDecryptOAEP(privKeyObj *Pkcs11KeyFileObject, ciphertext []byte, hashalg string) ([]byte, error) {
+ oldenv, err := setEnvVars(privKeyObj.Uri.GetEnvMap())
+ if err != nil {
+ return nil, err
+ }
+ defer restoreEnv(oldenv)
+
+ p11ctx, session, err := pkcs11UriLogin(privKeyObj.Uri, true)
+ if err != nil {
+ return nil, err
+ }
+ defer pkcs11Logout(p11ctx, session)
+
+ keyid, label, err := pkcs11UriGetKeyIdAndLabel(privKeyObj.Uri)
+ if err != nil {
+ return nil, err
+ }
+
+ p11PrivKey, err := findObject(p11ctx, session, pkcs11.CKO_PRIVATE_KEY, keyid, label)
+ if err != nil {
+ return nil, err
+ }
+
+ var oaep *pkcs11.OAEPParams
+
+ // An empty string from the Hash in the JSON historically defaults to sha1.
+ switch hashalg {
+ case "sha1", "":
+ oaep = OAEPSha1Params
+ case "sha256":
+ oaep = OAEPSha256Params
+ default:
+ return nil, fmt.Errorf("Unsupported hash algorithm '%s' for decryption", hashalg)
+ }
+
+ err = p11ctx.DecryptInit(session, []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS_OAEP, oaep)}, p11PrivKey)
+ if err != nil {
+ return nil, fmt.Errorf("DecryptInit failed: %w", err)
+ }
+ plaintext, err := p11ctx.Decrypt(session, ciphertext)
+ if err != nil {
+ return nil, fmt.Errorf("Decrypt failed: %w", err)
+ }
+ return plaintext, err
+}
+
+//
+// The following part deals with the JSON formatted message for multiple pkcs11 recipients
+//
+
+// Pkcs11Blob holds the encrypted blobs for all recipients; this is what we will put into the image's annotations
+type Pkcs11Blob struct {
+ Version uint `json:"version"`
+ Recipients []Pkcs11Recipient `json:"recipients"`
+}
+
+// Pkcs11Recipient holds the b64-encoded and encrypted blob for a particular recipient
+type Pkcs11Recipient struct {
+ Version uint `json:"version"`
+ Blob string `json:"blob"`
+ Hash string `json:"hash,omitempty"`
+}
+
+// EncryptMultiple encrypts for one or multiple pkcs11 devices; the public keys passed to this function
+// may either be *rsa.PublicKey or *pkcs11uri.Pkcs11URI; the returned byte array is a JSON string of the
+// following format:
+// {
+// recipients: [ // recipient list
+// {
+// "version": 0,
+// "blob": <base64 encoded RSA OAEP encrypted blob>,
+// "hash": <hash used for OAEP other than 'sha256'>
+// } ,
+// {
+// "version": 0,
+// "blob": <base64 encoded RSA OAEP encrypted blob>,
+// "hash": <hash used for OAEP other than 'sha256'>
+// } ,
+// [...]
+// ]
+// }
+func EncryptMultiple(pubKeys []interface{}, data []byte) ([]byte, error) {
+ var (
+ ciphertext []byte
+ err error
+ pkcs11blob Pkcs11Blob = Pkcs11Blob{Version: 0}
+ hashalg string
+ )
+
+ for _, pubKey := range pubKeys {
+ switch pkey := pubKey.(type) {
+ case *rsa.PublicKey:
+ ciphertext, hashalg, err = rsaPublicEncryptOAEP(pkey, data)
+ case *Pkcs11KeyFileObject:
+ ciphertext, hashalg, err = publicEncryptOAEP(pkey, data)
+ default:
+ err = fmt.Errorf("Unsupported key object type for pkcs11 public key")
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ recipient := Pkcs11Recipient{
+ Version: 0,
+ Blob: base64.StdEncoding.EncodeToString(ciphertext),
+ Hash: hashalg,
+ }
+
+ pkcs11blob.Recipients = append(pkcs11blob.Recipients, recipient)
+ }
+ return json.Marshal(&pkcs11blob)
+}
+
+// Decrypt tries to decrypt one of the recipients' blobs using a pkcs11 private key.
+// The input pkcs11blobstr is a string with the following format:
+// {
+// recipients: [ // recipient list
+// {
+// "version": 0,
+// "blob": <base64 encoded RSA OAEP encrypted blob>,
+// "hash": <hash used for OAEP other than 'sha1'>
+// } ,
+// {
+// "version": 0,
+// "blob": <base64 encoded RSA OAEP encrypted blob>,
+// "hash": <hash used for OAEP other than 'sha1'>
+// } ,
+// [...]
+// }
+// Note: More recent versions of this code explicitly write 'sha1'
+// while older versions left it empty in case of 'sha1'.
+func Decrypt(privKeyObjs []*Pkcs11KeyFileObject, pkcs11blobstr []byte) ([]byte, error) {
+ pkcs11blob := Pkcs11Blob{}
+ err := json.Unmarshal(pkcs11blobstr, &pkcs11blob)
+ if err != nil {
+ return nil, fmt.Errorf("Could not parse Pkcs11Blob: %w", err)
+ }
+ switch pkcs11blob.Version {
+ case 0:
+ // latest supported version
+ default:
+ return nil, fmt.Errorf("found Pkcs11Blob with version %d but maximum supported version is 0", pkcs11blob.Version)
+ }
+ // since we do trial and error, collect all encountered errors
+ errs := ""
+
+ for _, recipient := range pkcs11blob.Recipients {
+ switch recipient.Version {
+ case 0:
+ // last supported version
+ default:
+ return nil, fmt.Errorf("found Pkcs11Recipient with version %d but maximum supported version is 0", recipient.Version)
+ }
+
+ ciphertext, err := base64.StdEncoding.DecodeString(recipient.Blob)
+ if err != nil || len(ciphertext) == 0 {
+ // This should never happen... we skip over decoding issues
+ errs += fmt.Sprintf("Base64 decoding failed: %s\n", err)
+ continue
+ }
+ // try all keys until one works
+ for _, privKeyObj := range privKeyObjs {
+ plaintext, err := privateDecryptOAEP(privKeyObj, ciphertext, recipient.Hash)
+ if err == nil {
+ return plaintext, nil
+ }
+ if uri, err2 := privKeyObj.Uri.Format(); err2 == nil {
+ errs += fmt.Sprintf("%s : %s\n", uri, err)
+ } else {
+ errs += fmt.Sprintf("%s\n", err)
+ }
+ }
+ }
+
+ return nil, fmt.Errorf("Could not find a pkcs11 key for decryption:\n%s", errs)
+}
diff --git a/crypto/pkcs11/pkcs11helpers_nocgo.go b/crypto/pkcs11/pkcs11helpers_nocgo.go
new file mode 100644
index 0000000..6cf0aa2
--- /dev/null
+++ b/crypto/pkcs11/pkcs11helpers_nocgo.go
@@ -0,0 +1,30 @@
+//go:build !cgo
+// +build !cgo
+
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package pkcs11
+
+import "fmt"
+
+func EncryptMultiple(pubKeys []interface{}, data []byte) ([]byte, error) {
+ return nil, fmt.Errorf("ocicrypt pkcs11 not supported on this build")
+}
+
+func Decrypt(privKeyObjs []*Pkcs11KeyFileObject, pkcs11blobstr []byte) ([]byte, error) {
+ return nil, fmt.Errorf("ocicrypt pkcs11 not supported on this build")
+}
diff --git a/crypto/pkcs11/pkcs11helpers_test.go b/crypto/pkcs11/pkcs11helpers_test.go
new file mode 100644
index 0000000..78f98af
--- /dev/null
+++ b/crypto/pkcs11/pkcs11helpers_test.go
@@ -0,0 +1,229 @@
+//go:build cgo
+// +build cgo
+
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package pkcs11
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/containers/ocicrypt/utils/softhsm"
+)
+
+var (
+ SOFTHSM_SETUP = "../../scripts/softhsm_setup"
+)
+
+func getPkcs11Config(t *testing.T) *Pkcs11Config {
+ // we need to provide a configuration file so that on the various distros
+ // the libsofthsm2.so will be found by searching directories
+ mdyaml := GetDefaultModuleDirectoriesYaml("")
+ config := fmt.Sprintf("module-directories:\n"+
+ "%s"+
+ "allowed-module-paths:\n"+
+ "%s", mdyaml, mdyaml)
+
+ p11conf, err := ParsePkcs11ConfigFile([]byte(config))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return p11conf
+}
+
+func TestParsePkcs11KeyFileGood(t *testing.T) {
+ data := `pkcs11:
+ uri: pkcs11:slot-id=2053753261?module-name=softhsm2&pin-value=1234
+`
+ if !IsPkcs11PrivateKey([]byte(data)) {
+ t.Fatalf("YAML should have been detected as pkcs11 private key")
+ }
+
+ p11keyfileobj, err := ParsePkcs11KeyFile([]byte(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ p11conf := getPkcs11Config(t)
+ p11keyfileobj.Uri.SetModuleDirectories(p11conf.ModuleDirectories)
+ p11keyfileobj.Uri.SetAllowedModulePaths(p11conf.ModuleDirectories)
+
+ module, err := p11keyfileobj.Uri.GetModule()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !strings.HasSuffix(module, "/libsofthsm2.so") {
+ t.Fatalf("Unexpect module '%s'", module)
+ }
+}
+
+func TestParsePkcs11KeyFileBad(t *testing.T) {
+ data := `pkcs11:
+ uri: foobar
+`
+ if IsPkcs11PrivateKey([]byte(data)) {
+ t.Fatalf("Malformed pkcs11 key file should not have been detected as a pkcs11 private key")
+ }
+
+ _, err := ParsePkcs11KeyFile([]byte(data))
+ if err == nil {
+ t.Fatalf("Parsing the malformed pkcs11 key file should have failed")
+ }
+
+ // Missing URI
+ data = `pkcs11:
+`
+ if IsPkcs11PrivateKey([]byte(data)) {
+ t.Fatalf("Malformed pkcs11 key file should not have been detected as a pkcs11 private key")
+ }
+
+ _, err = ParsePkcs11KeyFile([]byte(data))
+ if err == nil {
+ t.Fatalf("Parsing the malformed pkcs11 key file should have failed")
+ }
+}
+
+func TestPkcs11EncryptDecrypt(t *testing.T) {
+ // We always need the query attributes 'pin-value' and 'module-name'
+ // for SoftHSM2 the only other important attribute is 'object' (= the 'label')
+ shsm := softhsm.NewSoftHSMSetup()
+ p11pubkeyuristr, err := shsm.RunSoftHSMSetup(SOFTHSM_SETUP)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer shsm.RunSoftHSMTeardown(SOFTHSM_SETUP)
+
+ data := `pkcs11:
+ uri: ` + p11pubkeyuristr + `
+module:
+ env:
+ SOFTHSM2_CONF: ` + shsm.GetConfigFilename()
+
+ // deactivate the PIN value
+ pubkeydata := strings.Replace(data, "pin-value", "unused", 1)
+
+ p11pubkeyfileobj, err := ParsePkcs11KeyFile([]byte(pubkeydata))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testinput := "Hello World!"
+
+ p11conf := getPkcs11Config(t)
+ p11pubkeyfileobj.Uri.SetModuleDirectories(p11conf.ModuleDirectories)
+ p11pubkeyfileobj.Uri.SetAllowedModulePaths(p11conf.ModuleDirectories)
+
+ // SoftHSM 2.6.1 only supports OAEP with sha1
+ // https://github.com/opendnssec/SoftHSMv2/blob/7f99bedae002f0dd04ceeb8d86d59fc4a68a69a0/src/lib/SoftHSM.cpp#L3123-L3127
+ os.Setenv("OCICRYPT_OAEP_HASHALG", "sha1")
+
+ pubKeys := make([]interface{}, 1)
+ pubKeys[0] = p11pubkeyfileobj
+ p11json, err := EncryptMultiple(pubKeys, []byte(testinput))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ p11privkeyfileobj, err := ParsePkcs11KeyFile([]byte(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+ p11privkeyfileobj.Uri.SetModuleDirectories(p11conf.ModuleDirectories)
+ p11privkeyfileobj.Uri.SetAllowedModulePaths(p11conf.ModuleDirectories)
+
+ privKeys := make([]*Pkcs11KeyFileObject, 1)
+ privKeys[0] = p11privkeyfileobj
+ plaintext, err := Decrypt(privKeys, p11json)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(plaintext) != testinput {
+ t.Fatalf("plaintext '%s' is not expected '%s'", plaintext, testinput)
+ }
+}
+
+func TestPkcs11EncryptDecryptPubkey(t *testing.T) {
+ // We always need the query attributes 'pin-value' and 'module-name'
+ // for SoftHSM2 the only other important attribute is 'object' (= the 'label')
+ shsm := softhsm.NewSoftHSMSetup()
+ p11pubkeyuristr, err := shsm.RunSoftHSMSetup(SOFTHSM_SETUP)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer shsm.RunSoftHSMTeardown(SOFTHSM_SETUP)
+
+ pubkeypem, err := shsm.RunSoftHSMGetPubkey(SOFTHSM_SETUP)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ block, _ := pem.Decode([]byte(pubkeypem))
+ if block == nil {
+ t.Fatal("failed to parse PEM block containing the public key")
+ }
+ rsapubkey, err := x509.ParsePKIXPublicKey(block.Bytes)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testinput := "Hello World!"
+
+ // SoftHSM 2.6.1 only supports OAEP with sha1
+ // https://github.com/opendnssec/SoftHSMv2/blob/7f99bedae002f0dd04ceeb8d86d59fc4a68a69a0/src/lib/SoftHSM.cpp#L3123-L3127
+ os.Setenv("OCICRYPT_OAEP_HASHALG", "sha1")
+
+ pubKeys := make([]interface{}, 1)
+ pubKeys[0] = rsapubkey
+ p11json, err := EncryptMultiple(pubKeys, []byte(testinput))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ data := `pkcs11:
+ uri: ` + p11pubkeyuristr + `
+module:
+ env:
+ SOFTHSM2_CONF: ` + shsm.GetConfigFilename()
+
+ p11keyfileobj, err := ParsePkcs11KeyFile([]byte(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ p11conf := getPkcs11Config(t)
+ p11keyfileobj.Uri.SetModuleDirectories(p11conf.ModuleDirectories)
+ p11keyfileobj.Uri.SetAllowedModulePaths(p11conf.ModuleDirectories)
+
+ // for SoftHSM we can just reuse the public key URI
+ privKeys := make([]*Pkcs11KeyFileObject, 1)
+ privKeys[0] = p11keyfileobj
+ plaintext, err := Decrypt(privKeys, p11json)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(plaintext) != testinput {
+ t.Fatalf("plaintext '%s' is not expected '%s'", plaintext, testinput)
+ }
+}
diff --git a/crypto/pkcs11/utils.go b/crypto/pkcs11/utils.go
new file mode 100644
index 0000000..231da23
--- /dev/null
+++ b/crypto/pkcs11/utils.go
@@ -0,0 +1,115 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package pkcs11
+
+import (
+ "fmt"
+ "os"
+ "runtime"
+ "strings"
+ "sync"
+)
+
+var (
+ envLock sync.Mutex
+)
+
+// setEnvVars sets the environment variables given in the map and locks the environment from
+// modification with the same function; if successful, you *must* call restoreEnv with the return
+// value from this function
+func setEnvVars(env map[string]string) ([]string, error) {
+ envLock.Lock()
+
+ if len(env) == 0 {
+ return nil, nil
+ }
+
+ oldenv := os.Environ()
+
+ for k, v := range env {
+ err := os.Setenv(k, v)
+ if err != nil {
+ restoreEnv(oldenv)
+ return nil, fmt.Errorf("Could not set environment variable '%s' to '%s': %w", k, v, err)
+ }
+ }
+
+ return oldenv, nil
+}
+
+func arrayToMap(elements []string) map[string]string {
+ o := make(map[string]string)
+
+ for _, element := range elements {
+ p := strings.SplitN(element, "=", 2)
+ if len(p) == 2 {
+ o[p[0]] = p[1]
+ }
+ }
+
+ return o
+}
+
+// restoreEnv restores the environment to be exactly as given in the array of strings
+// and unlocks the lock
+func restoreEnv(envs []string) {
+ if envs != nil && len(envs) >= 0 {
+ target := arrayToMap(envs)
+ curr := arrayToMap(os.Environ())
+
+ for nc, vc := range curr {
+ vt, ok := target[nc]
+ if !ok {
+ os.Unsetenv(nc)
+ } else if vc == vt {
+ delete(target, nc)
+ }
+ }
+
+ for nt, vt := range target {
+ os.Setenv(nt, vt)
+ }
+ }
+
+ envLock.Unlock()
+}
+
+func getHostAndOsType() (string, string, string) {
+ ht := ""
+ ot := ""
+ st := ""
+ switch runtime.GOOS {
+ case "linux":
+ ot = "linux"
+ st = "gnu"
+ switch runtime.GOARCH {
+ case "arm":
+ ht = "arm"
+ case "arm64":
+ ht = "aarch64"
+ case "amd64":
+ ht = "x86_64"
+ case "ppc64le":
+ ht = "powerpc64le"
+ case "riscv64":
+ ht = "riscv64"
+ case "s390x":
+ ht = "s390x"
+ }
+ }
+ return ht, ot, st
+}
diff --git a/docs/cex-ep11.md b/docs/cex-ep11.md
new file mode 100644
index 0000000..258c4b4
--- /dev/null
+++ b/docs/cex-ep11.md
@@ -0,0 +1,86 @@
+# OCIcrypt with Enterprise PKCS #11 on IBM CryptoExpress
+
+Note: This is a tutorial on using IBM CryptoExpress as an example of use of the ocicrypt library. This is not an endorsement or recommendation to use IBM CryptoExpress.
+
+OCIcrypt supports the use of an experimental PKCS #11-based protocol.
+The [main documentation on this topic](https://github.com/containers/ocicrypt/blob/main/docs/pkcs11.md) explains how to use this with SoftHSM2, a software emulation of a hardware security module (HSM).
+However, it is also possible to use an [IBM CryptoExpress HSM](https://www.ibm.com/security/cryptocards) in Enterprise PKCS #11 (EP11) mode, available for IBM Z and x64, together with the [openCryptoki library](https://github.com/opencryptoki/opencryptoki).
+This document provides some tips on how to set this up compared to the SoftHSM2 setup.
+
+This guide focuses on EP11 mode, but this should also be possible to accomplish in CCA mode and even in Accelerator mode (despite the unavailability of Secure Keys in the latter).
+
+The steps in this tutorial were tested with Ubuntu 20.04 on an IBM z15 LPAR with a CEX7S Crypto Card (IBM 4769).
+
+## Setup pre-requirements
+
+On Linux on IBM Z & LinuxONE, you can check your hardware with `lszcrypt` from [s390-tools](https://github.com/ibm-s390-linux/s390-tools) (package name varies among Linux distributions).
+You might have to `modprobe ap` if the appropriate module is not loaded.
+For Enterprise PKCS #11, you should have an online `EP11-Coproc`, which might already be configured.
+Otherwise, you must first assign adapters and domains in the Support Element/Hardware Management Console, and/or configure them as EP11 (`CEX?P`) rather than CCA or accelerator.
+Administrator certificates and master keys must also be set up.
+For more information, see IBM's documentation on [Preparing](https://www.ibm.com/docs/en/linux-on-systems?topic=stack-preparing-crypto-express-ep11-coprocessor) and [Setting a master key on the Crypto Express EP11 coprocessor](https://www.ibm.com/docs/en/linux-on-systems?topic=stack-setting-master-key-crypto-express-ep11-coprocessor).
+
+## Software requirements
+
+From your distribution, install e.g.
+```
+apt install -y gnutls-bin opencryptoki p11-kit
+```
+
+Additionally, you will need the EP11 host library (`libep11` or `ep11-host` depending on package format), which is not in any distribution.
+You can get it from [the software downloads for cryptographic hardware](https://www.ibm.com/security/cryptocards/pciecc4/software) and install it with `dpkg -i`/`rpm -i`.
+
+## openCryptoki configuration
+
+Register openCryptoki as a module for p11-kit by entering the path of the `libopencryptoki.so` file (location varies among Linux distributions), e.g.
+```
+cat << EOF | sudo tee /etc/pkcs11/modules/opencryptoki.module
+module: /usr/lib/s390x-linux-gnu/pkcs11/libopencryptoki.so
+EOF
+```
+
+Depending on the hardware you have available, you should now see an entry with a model name of `IBM EP11Tok` in the output of
+```
+p11tool --list-tokens
+```
+
+(requires root to see all), e.g.
+```
+...
+Token 4:
+ URL: pkcs11:model=IBM%20EP11Tok;manufacturer=IBM%20Corp.;serial=93AAA1XX22347171;token=IBM%20OS%20PKCS%2311
+ Label: IBM OS PKCS#11
+ Type: Generic token
+ Flags: RNG, Requires login, Uninitialized
+ Manufacturer: IBM Corp.
+ Model: IBM EP11Tok
+ Serial: 93AAA1XX22347171
+ Module: /usr/lib/s390x-linux-gnu/pkcs11/libopencryptoki.so
+```
+
+You will need the appropriate `URL:` field throughout the process.
+All entries listed by `pkcsconf -t` should also be here.
+You can customize the configuration in `/etc/opencryptoki`, e.g. to assign specific adapters to openCryptoki (and, consequently, p11) slots.
+You will have to `systemctl restart pkcsslotd` thereafter.
+Again, see the [IBM Documentation](https://www.ibm.com/docs/en/linux-on-systems?topic=315-configuring-opencryptoki-ep11-support) for more information.
+
+## Preparing the token with a PIN
+
+All tokens require a PIN to use (similar to SoftHSM2).
+Assuming the URL from above (yours will be different!), it is set with:
+```
+p11tool --initialize-pin 'pkcs11:model=IBM%20EP11Tok;manufacturer=IBM%20Corp.;serial=93AAA1XX22347171;token=IBM%20OS%20PKCS%2311'
+```
+
+This will require the Security Officer PIN, which is 87654321 by default (although it should be changed in a production environment).
+
+## Continuing as usual
+
+From here on out, the steps are identical with the steps documented in the SoftHSM2 PKCS #11 tutorial.
+Continue at ["Now create the private RSA key"](https://github.com/containers/ocicrypt/blob/main/docs/pkcs11.md#now-create-the-private-rsa-key), using the appropriate URL as above.
+
+### Some things to watch out for
+
+- The `module-name` should be `opencryptoki` rather than `softhsm2`.
+- You will not need the `SOFTHSM2_CONF` entry in the key configuration.
+- `OCICRYPT_CONFIG` can be set to `internal`.
diff --git a/docs/keyprovider.md b/docs/keyprovider.md
new file mode 100644
index 0000000..a7fa315
--- /dev/null
+++ b/docs/keyprovider.md
@@ -0,0 +1,74 @@
+# Ocicrypt keyprovider protocol
+
+Ocicrypt supports the use of a key-provider protocol. This allows the ability to encrypt and decrypt container image using the key that can be retrieved from any key management service.
+The config file consists for list of protocols that can be used for either encryption or decryption. User can implement either a binary executable or grpc server for fetching the wrapped(during image encryption) or unwrapped key(during image decryption) using any key management service.
+
+## Example of config file
+
+```code
+ {
+ "key-providers": {
+ "isecl": {
+ "path": "/usr/lib/ocicrypt-isecl",
+ "args": []
+ },
+ "keyprotect": {
+ "path": "/usr/lib/ocicrypt-keyprotect",
+ "args": []
+ },
+ "keyvault": {
+ "grpc": "localhost:50051"
+ }
+ }
+ }
+```
+
+## Encrpyting/Decrypting examples
+
+1. Build a sample golang application/binary -> https://github.com/lumjjb/simple-ocicrypt-keyprovider
+
+2. Configure the ${HOME}/ocirypt.conf like below
+```code
+ $ cat /home/vagrant/ocicrypt.conf
+ {
+ "key-providers": {
+ "simplecrypt": {
+ "cmd": {
+ "path":"/home/vagrant/simplecrypt",
+ "args": []
+ }
+ }
+ }
+ }
+```
+
+3. Prepare a sample image to encrypt or use an already built image from any public/private registry by pulling it into local repository and Image should be oci complaint
+```code
+ $ skopeo copy docker://docker.io/library/alpine:latest oci:alpine
+ Getting image source signatures
+ Copying blob 05e7bc50f07f done
+ Copying config 5c41fd95ee done
+ Writing manifest to image destination
+ Storing signatures
+```
+
+4. Encrypt the image as shown below
+```code
+ $ OCICRYPT_KEYPROVIDER_CONFIG=/home/vagrant/ocicrypt.conf bin/skopeo copy --encryption-key provider:simplecrypt:abc oci:alpine oci:encrypted
+ Getting image source signatures
+ Copying blob 05e7bc50f07f done
+ Copying config 5c41fd95ee done
+ Writing manifest to image destination
+ Storing signatures
+```
+
+5. Decrypt the image as shown below
+```code
+ $ OCICRYPT_KEYPROVIDER_CONFIG=/home/vagrant/ocicrypt.conf bin/skopeo copy --decryption-key provider:simplecrypt:extra-params oci:encrypted oci:decrypted
+
+ Getting image source signatures
+ Copying blob 4029b2314db9 done
+ Copying config 5c41fd95ee done
+ Writing manifest to image destination
+ Storing signatures
+```
diff --git a/docs/pkcs11.md b/docs/pkcs11.md
new file mode 100644
index 0000000..465be80
--- /dev/null
+++ b/docs/pkcs11.md
@@ -0,0 +1,320 @@
+# Ocicrypt Pkcs11 (Experimental)
+
+Ocicrypt supports the use of an experimental pkcs11-based protocol. This allows the ability to encrypt a container image so that it can be decrypted by a key which resides in a Hardware Security Module (HSM). In this document, we will go through a tutorial on how to setup and use this capability with a software emulated HSM, SoftHSM. See [this guide](https://github.com/containers/ocicrypt/blob/main/docs/cex-ep11.md) on how to do this with an IBM CryptoExpress HSM instead.
+
+This tutorial is done on Ubuntu.
+
+# Setting up SoftHSM
+
+## Requirements
+
+On top of the generic ocicrypt requirements, install the following packages:
+```
+sudo apt install -y softhsm2 p11-kit gnutls-bin
+```
+
+
+## Create SoftHSM Configuration
+
+We will setup the configurations and folders for SoftHSM. We will use the home directory to store this configuration.
+
+### Create config directories and sub directories
+```
+mkdir -p ${HOME}/.config/softhsm2/tokens
+```
+### Write configuration file
+```
+cat > ${HOME}/.config/softhsm2/softhsm2.conf <<EOF
+directories.tokendir = ${HOME}/.config/softhsm2/tokens
+objectstore.backend = file
+log.level = DEBUG
+slots.removable = false
+EOF
+```
+
+
+# Creating a pkcs11 key in SoftHSM
+
+To create a key, we need to first create a token, and then generate the private key for the token.
+
+# Create token for SoftHSM
+
+Set the configuration file to the one created above. All subsequent commands should be run with this environment variable set.
+```
+export SOFTHSM2_CONF=${HOME}/.config/softhsm2/softhsm2.conf
+```
+
+Create a token
+```
+softhsm2-util --init-token --free --label mytoken --pin my-pin --so-pin my-pin
+```
+The output should look like the following:
+```
+Slot 0 has a free/uninitialized token.
+The token has been initialized and is reassigned to slot 1151822331
+```
+
+We can then verify the token is created.
+
+```
+p11tool --list-tokens
+```
+
+Output:
+```
+Token 0:
+ URL: pkcs11:model=p11-kit-trust;manufacturer=PKCS%2311%20Kit;serial=1;token=System%20Trust
+ Label: System Trust
+ Type: Trust module
+ Flags: uPIN uninitialized
+ Manufacturer: PKCS#11 Kit
+ Model: p11-kit-trust
+ Serial: 1
+ Module: p11-kit-trust.so
+
+
+Token 1:
+ URL: pkcs11:model=SoftHSM%20v2;manufacturer=SoftHSM%20project;serial=ee777786c4a769fb;token=mytoken
+ Label: mytoken
+ Type: Generic token
+ Flags: RNG, Requires login
+ Manufacturer: SoftHSM project
+ Model: SoftHSM v2
+ Serial: ee777786c4a769fb
+ Module: /usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so
+```
+
+We will find the token that we created with label mytoken and copy the "Token URI", which is refers to the `URL` field of a token. In this case, the pkcs url is `pkcs11:model=SoftHSM%20v2;manufacturer=SoftHSM%20project;serial=ee777786c4a769fb;token=mytoken`. We note that this is the token for `SoftHSM v2` (the later token).
+
+# Now create the private RSA key
+
+Next we have to create a private key for this token.
+
+Note: For p11tool commands we need the `GNUTLS_PIN` env. variable (could also pass via command line but this is more secure). For this tutorial, we will use the environment variable.
+
+Run
+```
+export GNUTLS_PIN=my-pin
+```
+
+We will now create a key pair for this associated token in the HSM.
+
+Note: ALWAYS protect pkcs11 URIs with single quotes. (i.e. '<uri>'), If you are not using the `GNUTLS_PIN` environment variable, when prompted for the pin, enter the pin used above (in our example, it is `my-pin`).
+
+Run
+```
+p11tool --login --generate-privkey=rsa --label imagekey 'pkcs11:model=SoftHSM%20v2;manufacturer=SoftHSM%20project;serial=ee777786c4a769fb;token=mytoken'
+```
+
+The output will be contain the associated public key for the generated private key. Take note of this. We will store it as `pubkey.pem`
+```
+warning: no --outfile was specified and the generated public key will be printed on screen.
+Generating an RSA key...
+-----BEGIN PUBLIC KEY-----
+MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxDDWvGOBdUhqvujrk3ty
+WRGNKOP2VR0TQW1uUP6bECLpqFIPljf8HzniNR28MscP5fp/qsU6XwoMZWJh0VDK
+rzh0MzRAoSi0XMMtdYqoKjpWJxZWkTsahhjv2N/2khduvoIFwfL9Xoy+AjWP01no
+EFC1ctXv0jP6V+HkSfW3GQVYMg35ix5UKBeHdhI1GAA0Y0V1w+O1CJONtHBVbtb1
+ts9nU3Eq5LUujHVYJJ8YbnfXU7AQQ7mVUgsA0S6A9YT0FY7ljU5K47zCYwO++q+i
+ui6lGabbiCNjJhiUrhQTCyeOqaKLYfxBAWOuuBLKnWaKnL2baC1h0A5H9AHcPQYh
+fRHARf8SJ4Zo+aWEFMTK8xStlg5aHHhm1dKVUKamIf6rDJoMOI1HczzkKXe20whU
+G+rQj80brlTKVXtAlPLlb7zgM9YI3fPXlGSqgbVHakYiN7Hkj26c+1gwK9dShZ3p
+Ecq2jPcQiQpyOEr2xppwmPa5daJm00Syr3wuXxu4J0+HAgMBAAE=
+-----END PUBLIC KEY-----
+```
+
+We now need to find the pkcs11 URI of the private key by using the URI of the token
+```
+p11tool --login --list-privkeys 'pkcs11:model=SoftHSM%20v2;manufacturer=SoftHSM%20project;serial=ee777786c4a769fb;token=mytoken'
+```
+
+The output will be like the following:
+```
+Token 'mytoken' with URL 'pkcs11:model=SoftHSM%20v2;manufacturer=SoftHSM%20project;serial=ee777786c4a769fb;token=mytoken' requires user PIN
+Enter PIN:
+Object 0:
+ URL: pkcs11:model=SoftHSM%20v2;manufacturer=SoftHSM%20project;serial=ee777786c4a769fb;token=mytoken;id=%A9%8B%58%89%89%C7%A9%01%59%5A%85%9D%5D%6A%FF%C3%E4%57%E2%16;object=imagekey;type=private
+ Type: Private key (RSA-3072)
+ Label: imagekey
+ Flags: CKA_WRAP/UNWRAP; CKA_PRIVATE; CKA_NEVER_EXTRACTABLE; CKA_SENSITIVE;
+ ID: a9:8b:58:89:89:c7:a9:01:59:5a:85:9d:5d:6a:ff:c3:e4:57:e2:16
+```
+
+Now take the "URL" field from the object above "pkcs11:model=SoftHSM%20v2;manufacturer=SoftHSM%20project;serial=ee777786c4a769fb;token=mytoken;id=%A9%8B%58%89%89%C7%A9%01%59%5A%85%9D%5D%6A%FF%C3%E4%57%E2%16;object=imagekey;type=private",
+and append "?pin-value=my-pin&module-name=softhsm2" to it, getting the following. We will be using this in the next step. We refer to this as "KEY_URI".
+
+```
+pkcs11:model=SoftHSM%20v2;manufacturer=SoftHSM%20project;serial=ee777786c4a769fb;token=mytoken;id=%A9%8B%58%89%89%C7%A9%01%59%5A%85%9D%5D%6A%FF%C3%E4%57%E2%16;object=imagekey;type=private?pin-value=my-pin&module-name=softhsm2
+```
+
+
+# Setting up PKCS11 for ocicrypt
+
+
+# Configuring pkcs11 modules for ocicrypt users
+
+In order to use pkcs11 with the ocicrypt library, there are several configuration and key conventions that need to be noted.
+
+Encrypting/decrypting with ocicrypt's pkcs11 keywrap module consists of two parts. One is the metadata of the key to use for encryption/decryption (i.e. similar to how keys are passed in via using other ocicrypt protocols such as jwe:, pkcs11:), and configuring the HSM modules on the host.
+
+## Creating pkcs11 key configuration.
+
+This is the representation of the key that the key-wrap module will use to talk to the HSM. It can be passed in like any other protocol key, i.e. pkcs11:myPkcs11Key.yaml. Note that this key can act as a private key and public key for ocicrypt. It is also possible to encrypt with a regular public key (as was output in the above step when generating the key).
+
+```
+cat > myPkcs11Key.yaml <<EOF
+pkcs11:
+ uri: pkcs11:model=SoftHSM%20v2;manufacturer=SoftHSM%20project;serial=ee777786c4a769fb;token=mytoken;id=%A9%8B%58%89%89%C7%A9%01%59%5A%85%9D%5D%6A%FF%C3%E4%57%E2%16;object=imagekey;type=private?pin-value=my-pin&module-name=softhsm2
+module:
+ env:
+ SOFTHSM2_CONF: ${HOME}/.config/softhsm2/softhsm2.conf
+EOF
+```
+
+## Configuring HSM modules
+
+Because communication with HSM modules are usually done with a external module, there is an additional configuration to tell the user of ocicrypt how to talk to the HSM modules on the host. This is also important to configure correctly so that only authorized modules are run.
+
+This configuration is done via an environment variable `OCICRPYT_CONFIG`. Here is the behavior of this configuration:
+- If the environment variable is not set, it indicates that no HSM modules should be allowed
+- If the environment variable is set to "internal", it uses policy that allows to access most pkcs11 modules. It holds default module search paths that should cover many distros ([details here](https://github.com/containers/ocicrypt/blob/2ddd51f10d6d15ce99e020ec35729ea741d32f2a/crypto/pkcs11/pkcs11helpers.go#L134))
+- Else, it is treated as a filepath, where it contains the configuration of where modules are, and which are allowed. More details on how to configure this can be seen [here](https://github.com/containers/ocicrypt/blob/master/config/pkcs11config/config.go#L38).
+
+
+
+# Encrpyting/Decrypting examples
+
+We will show how this can be used with users of ocicrypt. After performing the steps above, we are ready to encrypt/decrypt with the pkcs11 protocol. The capabilities that the tools provide will be:
+
+- Encrypting an image with ocicrypt pkcs11 protocol using a public key (PEM)
+- Encrypting an image with ocicrypt pkcs11 protocol using a pkcs11 key configuration (requires HSM access)
+- Decrypting an image with ocicrypt pkcs11 protocol using a pkcs11 key configuration (requires HSM access)
+
+We will go through 3 consumers of the ocicrypt library.
+- [containerd/imgcrypt](http://github.com/containerd/imgcrypt)
+- [skopeo](https://github.com/containers/skopeo)
+- [buildah](https://github.com/containers/buildah)
+
+NOTE: only builds that use ocicrypt v1.1.0 and above will have pkcs11 experimental support.
+
+
+We remember that we created two files above, pubkey.pem and `myPkcs11Key.yaml`. For the following command executions, we assume that the plaintext image has already been downloaded. We are using the image `docker.io/library/alpine:latest`.
+
+
+## containerd/imgcrypt
+
+With containerd imgcrypt, the tool to encrypt/decrypt images is `ctr-enc`.
+
+### Encryping with Public Key
+
+```
+$ OCICRYPT_CONFIG=internal ./bin/ctr-enc images encrypt --recipient pkcs11:myPkcs11Key.yaml docker.io/library/alpine:latest alpine.enc.pkcs11key:latest
+Encrypting docker.io/library/alpine:latest to alpine.enc.pkcs11key:latest
+Note: Pkcs11 support is currently experimental
+```
+
+### Encryping with PKCS11 Key Configuration
+
+```
+$ OCICRYPT_CONFIG=internal ./bin/ctr-enc images encrypt --recipient pkcs11:pubkey.pem docker.io/library/alpine:latest alpine.enc.pkcs11pubkey:latest
+Encrypting docker.io/library/alpine:latest to alpine.enc.pkcs11pubkey:latest
+Note: Pkcs11 support is currently experimental
+```
+
+
+### Decrypting with both images encrypted above with PKCS11 Key Configuration
+
+```
+$ OCICRYPT_CONFIG=internal ./bin/ctr-enc images decrypt --key myPkcs11Key.yaml alpine.enc.pkcs11key:latest alpine.dec.pkcs11key:latest
+Decrypting alpine.enc.pkcs11key:latest to alpine.dec.pkcs11key:latest
+
+$ OCICRYPT_CONFIG=internal ./bin/ctr-enc images decrypt --key myPkcs11Key.yaml alpine.enc.pkcs11pubkey:latest alpine.dec.pkcs11pubkey:latest
+Decrypting alpine.enc.pkcs11pubkey:latest to alpine.dec.pkcs11pubkey:latest
+```
+
+## skopeo
+
+### Encryping with Public Key
+
+```
+$ OCICRYPT_CONFIG=internal skopeo copy --encryption-key pkcs11:pubkey.pem oci:alpine:latest oci:alpine_enc_pkcs11pubkey:latest
+Calling create crypto config
+Getting image source signatures
+Copying blob df20fa9351a1 done
+Copying config 0f5f445df8 done
+Writing manifest to image destination
+Storing signatures
+```
+
+### Encryping with PKCS11 Key Configuration
+
+```
+$ OCICRYPT_CONFIG=internal skopeo copy --encryption-key pkcs11:myPkcs11Key.yaml oci:alpine:latest oci:alpine_enc_pkcs11key:latest
+Calling create crypto config
+Getting image source signatures
+Copying blob df20fa9351a1 done
+Copying config 0f5f445df8 done
+Writing manifest to image destination
+Storing signatures
+```
+
+
+### Decrypting with both images encrypted above with PKCS11 Key Configuration
+
+```
+$ OCICRYPT_CONFIG=internal skopeo copy --decryption-key myPkcs11Key.yaml oci:alpine_enc_pkcs11pubkey:latest oci:alpine_dec_pkcs11pubkey:latest
+Getting image source signatures
+Copying blob 32176ff73954 done
+Copying config 0f5f445df8 done
+Writing manifest to image destination
+Storing signatures
+
+
+$ OCICRYPT_CONFIG=internal skopeo copy --decryption-key myPkcs11Key.yaml oci:alpine_enc_pkcs11key:latest oci:alpine_dec_pkcs11key:latest
+Getting image source signatures
+Copying blob 31c20e72694c done
+Copying config 0f5f445df8 done
+Writing manifest to image destination
+Storing signatures
+```
+
+## buildah
+
+
+### Encryping with Public Key
+```
+$ OCICRYPT_CONFIG=internal ./bin/buildah push --encryption-key pkcs11:pubkey.pem docker.io/library/alpine:latest oci:alpine_enc_pkcs11pubkey:latest
+Getting image source signatures
+Copying blob 50644c29ef5a done
+Copying config 0f5f445df8 done
+Writing manifest to image destination
+Storing signatures
+```
+### Encryping with PKCS11 Key Configuration
+```
+$ OCICRYPT_CONFIG=internal ./bin/buildah push --encryption-key pkcs11:myPkcs11Key.yaml docker.io/library/alpine:latest oci:alpine_enc_pkcs11key:latest
+Getting image source signatures
+Copying blob 50644c29ef5a done
+Copying config 0f5f445df8 done
+Writing manifest to image destination
+Storing signatures
+```
+
+### Decrypting with both images encrypted above with PKCS11 Key Configuration
+```
+$ OCICRYPT_CONFIG=internal ./bin/buildah pull --decryption-key myPkcs11Key.yaml oci:alpine_enc_pkcs11pubkey:latest
+Getting image source signatures
+Copying blob dc5e8c1e77b5 done
+Copying config 0f5f445df8 done
+Writing manifest to image destination
+Storing signatures
+0f5f445df8ccbd8a062ad3d02d459e8549d9998c62a5b7cbf77baf68aa73bf5b
+
+$ OCICRYPT_CONFIG=internal ./bin/buildah pull --decryption-key myPkcs11Key.yaml oci:alpine_enc_pkcs11key:latest
+Getting image source signatures
+Copying blob 9f61ec599643 done
+Copying config 0f5f445df8 done
+Writing manifest to image destination
+Storing signatures
+0f5f445df8ccbd8a062ad3d02d459e8549d9998c62a5b7cbf77baf68aa73bf5b
+```
diff --git a/encryption.go b/encryption.go
new file mode 100644
index 0000000..b6fa9db
--- /dev/null
+++ b/encryption.go
@@ -0,0 +1,356 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ocicrypt
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/containers/ocicrypt/blockcipher"
+ "github.com/containers/ocicrypt/config"
+ keyproviderconfig "github.com/containers/ocicrypt/config/keyprovider-config"
+ "github.com/containers/ocicrypt/keywrap"
+ "github.com/containers/ocicrypt/keywrap/jwe"
+ "github.com/containers/ocicrypt/keywrap/keyprovider"
+ "github.com/containers/ocicrypt/keywrap/pgp"
+ "github.com/containers/ocicrypt/keywrap/pkcs11"
+ "github.com/containers/ocicrypt/keywrap/pkcs7"
+ "github.com/opencontainers/go-digest"
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+ log "github.com/sirupsen/logrus"
+)
+
+// EncryptLayerFinalizer is a finalizer run to return the annotations to set for
+// the encrypted layer
+type EncryptLayerFinalizer func() (map[string]string, error)
+
+func init() {
+ keyWrappers = make(map[string]keywrap.KeyWrapper)
+ keyWrapperAnnotations = make(map[string]string)
+ RegisterKeyWrapper("pgp", pgp.NewKeyWrapper())
+ RegisterKeyWrapper("jwe", jwe.NewKeyWrapper())
+ RegisterKeyWrapper("pkcs7", pkcs7.NewKeyWrapper())
+ RegisterKeyWrapper("pkcs11", pkcs11.NewKeyWrapper())
+ ic, err := keyproviderconfig.GetConfiguration()
+ if err != nil {
+ log.Error(err)
+ } else if ic != nil {
+ for provider, attrs := range ic.KeyProviderConfig {
+ RegisterKeyWrapper("provider."+provider, keyprovider.NewKeyWrapper(provider, attrs))
+ }
+ }
+}
+
+var keyWrappers map[string]keywrap.KeyWrapper
+var keyWrapperAnnotations map[string]string
+
+// RegisterKeyWrapper allows to register key wrappers by their encryption scheme
+func RegisterKeyWrapper(scheme string, iface keywrap.KeyWrapper) {
+ keyWrappers[scheme] = iface
+ keyWrapperAnnotations[iface.GetAnnotationID()] = scheme
+}
+
+// GetKeyWrapper looks up the encryptor interface given an encryption scheme (gpg, jwe)
+func GetKeyWrapper(scheme string) keywrap.KeyWrapper {
+ return keyWrappers[scheme]
+}
+
+// GetWrappedKeysMap returns a map of wrappedKeys as values in a
+// map with the encryption scheme(s) as the key(s)
+func GetWrappedKeysMap(desc ocispec.Descriptor) map[string]string {
+ wrappedKeysMap := make(map[string]string)
+
+ for annotationsID, scheme := range keyWrapperAnnotations {
+ if annotation, ok := desc.Annotations[annotationsID]; ok {
+ wrappedKeysMap[scheme] = annotation
+ }
+ }
+ return wrappedKeysMap
+}
+
+// EncryptLayer encrypts the layer by running one encryptor after the other
+func EncryptLayer(ec *config.EncryptConfig, encOrPlainLayerReader io.Reader, desc ocispec.Descriptor) (io.Reader, EncryptLayerFinalizer, error) {
+ var (
+ encLayerReader io.Reader
+ err error
+ encrypted bool
+ bcFin blockcipher.Finalizer
+ privOptsData []byte
+ pubOptsData []byte
+ )
+
+ if ec == nil {
+ return nil, nil, errors.New("EncryptConfig must not be nil")
+ }
+
+ for annotationsID := range keyWrapperAnnotations {
+ annotation := desc.Annotations[annotationsID]
+ if annotation != "" {
+ privOptsData, err = decryptLayerKeyOptsData(&ec.DecryptConfig, desc)
+ if err != nil {
+ return nil, nil, err
+ }
+ pubOptsData, err = getLayerPubOpts(desc)
+ if err != nil {
+ return nil, nil, err
+ }
+ // already encrypted!
+ encrypted = true
+ }
+ }
+
+ if !encrypted {
+ encLayerReader, bcFin, err = commonEncryptLayer(encOrPlainLayerReader, desc.Digest, blockcipher.AES256CTR)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ encLayerFinalizer := func() (map[string]string, error) {
+ // If layer was already encrypted, bcFin should be nil, use existing optsData
+ if bcFin != nil {
+ opts, err := bcFin()
+ if err != nil {
+ return nil, err
+ }
+ privOptsData, err = json.Marshal(opts.Private)
+ if err != nil {
+ return nil, fmt.Errorf("could not JSON marshal opts: %w", err)
+ }
+ pubOptsData, err = json.Marshal(opts.Public)
+ if err != nil {
+ return nil, fmt.Errorf("could not JSON marshal opts: %w", err)
+ }
+ }
+
+ newAnnotations := make(map[string]string)
+ keysWrapped := false
+ if len(keyWrapperAnnotations) == 0 {
+ return nil, errors.New("missing Annotations needed for decryption")
+ }
+ for annotationsID, scheme := range keyWrapperAnnotations {
+ b64Annotations := desc.Annotations[annotationsID]
+ keywrapper := GetKeyWrapper(scheme)
+ b64Annotations, err = preWrapKeys(keywrapper, ec, b64Annotations, privOptsData)
+ if err != nil {
+ return nil, err
+ }
+ if b64Annotations != "" {
+ keysWrapped = true
+ newAnnotations[annotationsID] = b64Annotations
+ }
+ }
+
+ if !keysWrapped {
+ return nil, errors.New("no wrapped keys produced by encryption")
+ }
+ newAnnotations["org.opencontainers.image.enc.pubopts"] = base64.StdEncoding.EncodeToString(pubOptsData)
+
+ if len(newAnnotations) == 0 {
+ return nil, errors.New("no encryptor found to handle encryption")
+ }
+
+ return newAnnotations, err
+ }
+
+ // if nothing was encrypted, we just return encLayer = nil
+ return encLayerReader, encLayerFinalizer, err
+
+}
+
+// preWrapKeys calls WrapKeys and handles the base64 encoding and concatenation of the
+// annotation data
+func preWrapKeys(keywrapper keywrap.KeyWrapper, ec *config.EncryptConfig, b64Annotations string, optsData []byte) (string, error) {
+ newAnnotation, err := keywrapper.WrapKeys(ec, optsData)
+ if err != nil || len(newAnnotation) == 0 {
+ return b64Annotations, err
+ }
+ b64newAnnotation := base64.StdEncoding.EncodeToString(newAnnotation)
+ if b64Annotations == "" {
+ return b64newAnnotation, nil
+ }
+ return b64Annotations + "," + b64newAnnotation, nil
+}
+
+// DecryptLayer decrypts a layer trying one keywrap.KeyWrapper after the other to see whether it
+// can apply the provided private key
+// If unwrapOnly is set we will only try to decrypt the layer encryption key and return
+func DecryptLayer(dc *config.DecryptConfig, encLayerReader io.Reader, desc ocispec.Descriptor, unwrapOnly bool) (io.Reader, digest.Digest, error) {
+ if dc == nil {
+ return nil, "", errors.New("DecryptConfig must not be nil")
+ }
+ privOptsData, err := decryptLayerKeyOptsData(dc, desc)
+ if err != nil || unwrapOnly {
+ return nil, "", err
+ }
+
+ var pubOptsData []byte
+ pubOptsData, err = getLayerPubOpts(desc)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return commonDecryptLayer(encLayerReader, privOptsData, pubOptsData)
+}
+
+func decryptLayerKeyOptsData(dc *config.DecryptConfig, desc ocispec.Descriptor) ([]byte, error) {
+ privKeyGiven := false
+ errs := ""
+ if len(keyWrapperAnnotations) == 0 {
+ return nil, errors.New("missing Annotations needed for decryption")
+ }
+ for annotationsID, scheme := range keyWrapperAnnotations {
+ b64Annotation := desc.Annotations[annotationsID]
+ if b64Annotation != "" {
+ keywrapper := GetKeyWrapper(scheme)
+
+ if keywrapper.NoPossibleKeys(dc.Parameters) {
+ continue
+ }
+
+ if len(keywrapper.GetPrivateKeys(dc.Parameters)) > 0 {
+ privKeyGiven = true
+ }
+ optsData, err := preUnwrapKey(keywrapper, dc, b64Annotation)
+ if err != nil {
+ // try next keywrap.KeyWrapper
+ errs += fmt.Sprintf("%s\n", err)
+ continue
+ }
+ if optsData == nil {
+ // try next keywrap.KeyWrapper
+ continue
+ }
+ return optsData, nil
+ }
+ }
+ if !privKeyGiven {
+ return nil, fmt.Errorf("missing private key needed for decryption:\n%s", errs)
+ }
+ return nil, fmt.Errorf("no suitable key unwrapper found or none of the private keys could be used for decryption:\n%s", errs)
+}
+
+func getLayerPubOpts(desc ocispec.Descriptor) ([]byte, error) {
+ pubOptsString := desc.Annotations["org.opencontainers.image.enc.pubopts"]
+ if pubOptsString == "" {
+ return json.Marshal(blockcipher.PublicLayerBlockCipherOptions{})
+ }
+ return base64.StdEncoding.DecodeString(pubOptsString)
+}
+
+// preUnwrapKey decodes the comma separated base64 strings and calls the Unwrap function
+// of the given keywrapper with it and returns the result in case the Unwrap functions
+// does not return an error. If all attempts fail, an error is returned.
+func preUnwrapKey(keywrapper keywrap.KeyWrapper, dc *config.DecryptConfig, b64Annotations string) ([]byte, error) {
+ if b64Annotations == "" {
+ return nil, nil
+ }
+ errs := ""
+ for _, b64Annotation := range strings.Split(b64Annotations, ",") {
+ annotation, err := base64.StdEncoding.DecodeString(b64Annotation)
+ if err != nil {
+ return nil, errors.New("could not base64 decode the annotation")
+ }
+ optsData, err := keywrapper.UnwrapKey(dc, annotation)
+ if err != nil {
+ errs += fmt.Sprintf("- %s\n", err)
+ continue
+ }
+ return optsData, nil
+ }
+ return nil, fmt.Errorf("no suitable key found for decrypting layer key:\n%s", errs)
+}
+
+// commonEncryptLayer is a function to encrypt the plain layer using a new random
+// symmetric key and return the LayerBlockCipherHandler's JSON in string form for
+// later use during decryption
+func commonEncryptLayer(plainLayerReader io.Reader, d digest.Digest, typ blockcipher.LayerCipherType) (io.Reader, blockcipher.Finalizer, error) {
+ lbch, err := blockcipher.NewLayerBlockCipherHandler()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ encLayerReader, bcFin, err := lbch.Encrypt(plainLayerReader, typ)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ newBcFin := func() (blockcipher.LayerBlockCipherOptions, error) {
+ lbco, err := bcFin()
+ if err != nil {
+ return blockcipher.LayerBlockCipherOptions{}, err
+ }
+ lbco.Private.Digest = d
+ return lbco, nil
+ }
+
+ return encLayerReader, newBcFin, err
+}
+
+// commonDecryptLayer decrypts an encrypted layer previously encrypted with commonEncryptLayer
+// by passing along the optsData
+func commonDecryptLayer(encLayerReader io.Reader, privOptsData []byte, pubOptsData []byte) (io.Reader, digest.Digest, error) {
+ privOpts := blockcipher.PrivateLayerBlockCipherOptions{}
+ err := json.Unmarshal(privOptsData, &privOpts)
+ if err != nil {
+ return nil, "", fmt.Errorf("could not JSON unmarshal privOptsData: %w", err)
+ }
+
+ lbch, err := blockcipher.NewLayerBlockCipherHandler()
+ if err != nil {
+ return nil, "", err
+ }
+
+ pubOpts := blockcipher.PublicLayerBlockCipherOptions{}
+ if len(pubOptsData) > 0 {
+ err := json.Unmarshal(pubOptsData, &pubOpts)
+ if err != nil {
+ return nil, "", fmt.Errorf("could not JSON unmarshal pubOptsData: %w", err)
+ }
+ }
+
+ opts := blockcipher.LayerBlockCipherOptions{
+ Private: privOpts,
+ Public: pubOpts,
+ }
+
+ plainLayerReader, opts, err := lbch.Decrypt(encLayerReader, opts)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return plainLayerReader, opts.Private.Digest, nil
+}
+
+// FilterOutAnnotations filters out the annotations belonging to the image encryption 'namespace'
+// and returns a map with those taken out
+func FilterOutAnnotations(annotations map[string]string) map[string]string {
+ a := make(map[string]string)
+ if len(annotations) > 0 {
+ for k, v := range annotations {
+ if strings.HasPrefix(k, "org.opencontainers.image.enc.") {
+ continue
+ }
+ a[k] = v
+ }
+ }
+ return a
+}
diff --git a/encryption_test.go b/encryption_test.go
new file mode 100644
index 0000000..f76de5d
--- /dev/null
+++ b/encryption_test.go
@@ -0,0 +1,143 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ocicrypt
+
+import (
+ "bytes"
+ "io"
+ "reflect"
+ "testing"
+
+ "github.com/containers/ocicrypt/config"
+ digest "github.com/opencontainers/go-digest"
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+var (
+ privateKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAnnExCL+jpTAgZzgEBwmR88ltusMBdsd4ZDO4M5NohTq0T6TP
+vJN99ud8ZAY8fVpN63TKD2enWy6imkSE3Cdp+yFgOntsg8WgdPF+8VQaNpn/g8LC
+rpWXpJuJIGzCSW5SlUt0OkvyeDvQFrKCqdI63H4cxXY5ly2HlTHJ1+YzEBSLONJY
+xsJt6/7cL7mJ7CR/9NZouft2xeNQto9JSqWUNCwUGdZOS1pMVkka/2tFmq8psciF
+YvCl6msX6haZ7IDq4/GfeteL8gp6t+hgyWmbJ5/h0hNEvAz4DVb5RnKYVLwWM/e9
+oQTw9WgKCqUZKe0+DKmuKYMH2g77oTvDtP8NvQIDAQABAoIBAC8tUQZj2ZxEGkHh
+wgE+bkECxzOHARaXClf7tmtVBxg0hJ/6WQizehxcjQNTgAtrKixj2A6CNKjH2A7L
+PCw5aCsooviG66bI36AykDPXcP61GAnpogJN9JtE3K3U9Hzc5qYhk3gQSSBX3vwD
+Jzjdqj0hJ/v72eYT3n0kGA+7MZUlsObpOouPAZMo72Bcvg2s20FLnKQCiGfH8zWv
+wJAnO5BhinwTPhi+01Xj9LePk/2bs3hEzH1/bA3DVmmaWp5H8RuaGuvQ6eX4EXir
+3xq9BjjYIK21dmD2R1S0jjez3/d2P7gENKGVItcakURWIn7IS0bYr8P2xIhnxQQV
+OTYgWRkCgYEAyboK1GDr+5KtCoAQE1e1Ntbi+x3R2sIjX8lGzLumd5ybqSMGH8V9
+6efRo7onuP5V66bDwxSeSFomOEF0rQZj3dpoEXkF95h8G65899okXMURsqjb6+wm
+xyFKZAJojJXsR076ES3tas/TgPVD/ZfcBYTU8Ssvfsi3uzeUrbuVL58CgYEAyRHq
+/1zsPDf3B7E8EsRA/YZJTfjzDlqXatTX5dnoUKBQH9nZut4qGWBwWPj2DKJwlGQr
+m12RIbM5uGvUe3csddzClp0zInDhvD/K3XlUthUfrYX209xaeOD6d4+7wd56SNEo
+AzhSobgmrITEAy8QA1u546ID+gFOQnzG17HelSMCgYEAsdmoep4I7//dOAi4I5WM
+WxERhTxBLJFFBso59X7rwUD9rB0I5TIFVRfhKGyTYPI7ZkvdBD1FX5y7XZW3/GRJ
+3+sTHXSJ4kU6Bl3MJ+jXbkMA23csjc/iUGX1ZD8LVgdIDYZ/ym2niCg63NNgYlBk
+1yjJZOciNLFZ62GRX6qmWRkCgYAYS7j4mFLXR+/qlwjqP5qWx9YtvMopztqDByr7
+VCRVMbncz2cWxGeT32pT5eldR3eRBrWaNWknCFAOL8FiFdlieIVuy5n1LGyqYY7y
+yglpYw4L2qcjnHm2J4E8VzrZxzdBezx5fyHE9sp9iCFjPRmTPk8s6VPPrr61G/yu
+7Yg2vwKBgAsJFi6zjqfUacMxB+Bb4Ehz7bqRAoeHZCH9MU2lGimjTUC322uQdu2H
+ZkYkHwYVP/RngrN7bhYwPahoDThKy+fIGJAuMhPXg6HVTSkcQSJJ/VeIB/DE4AVj
+8heezMN183u5gJvwaEj84fJvUEo/QdvG3NSjQptEGsXYSsE56wHu
+-----END RSA PRIVATE KEY-----`)
+
+ publicKey = []byte(`-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnnExCL+jpTAgZzgEBwmR
+88ltusMBdsd4ZDO4M5NohTq0T6TPvJN99ud8ZAY8fVpN63TKD2enWy6imkSE3Cdp
++yFgOntsg8WgdPF+8VQaNpn/g8LCrpWXpJuJIGzCSW5SlUt0OkvyeDvQFrKCqdI6
+3H4cxXY5ly2HlTHJ1+YzEBSLONJYxsJt6/7cL7mJ7CR/9NZouft2xeNQto9JSqWU
+NCwUGdZOS1pMVkka/2tFmq8psciFYvCl6msX6haZ7IDq4/GfeteL8gp6t+hgyWmb
+J5/h0hNEvAz4DVb5RnKYVLwWM/e9oQTw9WgKCqUZKe0+DKmuKYMH2g77oTvDtP8N
+vQIDAQAB
+-----END PUBLIC KEY-----`)
+)
+
+var (
+ ec *config.EncryptConfig
+ dc *config.DecryptConfig
+)
+
+func init() {
+ // TODO: Create various EncryptConfigs for testing purposes
+ dcparameters := make(map[string][][]byte)
+ parameters := make(map[string][][]byte)
+
+ parameters["pubkeys"] = [][]byte{publicKey}
+ dcparameters["privkeys"] = [][]byte{privateKey}
+ dcparameters["privkeys-passwords"] = [][]byte{{}}
+
+ ec = &config.EncryptConfig{
+ Parameters: parameters,
+ DecryptConfig: config.DecryptConfig{
+ Parameters: dcparameters,
+ },
+ }
+ dc = &config.DecryptConfig{
+ Parameters: dcparameters,
+ }
+}
+
+func TestEncryptLayer(t *testing.T) {
+ data := []byte("This is some text!")
+ desc := ocispec.Descriptor{
+ Digest: digest.FromBytes(data),
+ Size: int64(len(data)),
+ }
+
+ dataReader := bytes.NewReader(data)
+
+ encLayerReader, encLayerFinalizer, err := EncryptLayer(ec, dataReader, desc)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ encLayer := make([]byte, 1024)
+ encsize, err := encLayerReader.Read(encLayer)
+ if err != io.EOF {
+ t.Fatal("Expected EOF")
+ }
+ encLayerReaderAt := bytes.NewReader(encLayer[:encsize])
+
+ annotations, err := encLayerFinalizer()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(annotations) == 0 {
+ t.Fatal("No keys created for annotations")
+ }
+
+ newDesc := ocispec.Descriptor{
+ Annotations: annotations,
+ }
+
+ decLayerReader, _, err := DecryptLayer(dc, encLayerReaderAt, newDesc, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ decLayer := make([]byte, 1024)
+ decsize, err := decLayerReader.Read(decLayer)
+ if err != nil && err != io.EOF {
+ t.Fatal(err)
+ }
+
+ if !reflect.DeepEqual(decLayer[:decsize], data) {
+ t.Fatalf("Expected %v, got %v", data, decLayer)
+ }
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..1539d4e
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,29 @@
+module github.com/containers/ocicrypt
+
+go 1.20
+
+require (
+ github.com/go-jose/go-jose/v3 v3.0.0
+ github.com/golang/protobuf v1.5.3
+ github.com/miekg/pkcs11 v1.1.1
+ github.com/opencontainers/go-digest v1.0.0
+ github.com/opencontainers/image-spec v1.0.2
+ github.com/sirupsen/logrus v1.9.0
+ github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980
+ github.com/stretchr/testify v1.7.0
+ go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1
+ golang.org/x/crypto v0.14.0
+ golang.org/x/term v0.13.0
+ google.golang.org/grpc v1.56.3
+ gopkg.in/yaml.v3 v3.0.0
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ golang.org/x/net v0.17.0 // indirect
+ golang.org/x/sys v0.13.0 // indirect
+ golang.org/x/text v0.13.0 // indirect
+ google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
+ google.golang.org/protobuf v1.30.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..3b68e2a
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,60 @@
+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/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
+github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
+github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+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.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
+github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+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/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
+github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 h1:lIOOHPEbXzO3vnmx2gok1Tfs31Q8GQqKLc8vVqyQq/I=
+github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M=
+go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
+google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
+google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/gpg.go b/gpg.go
new file mode 100644
index 0000000..3912e82
--- /dev/null
+++ b/gpg.go
@@ -0,0 +1,432 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ocicrypt
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "regexp"
+ "strconv"
+ "strings"
+ "sync"
+
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+ "golang.org/x/term"
+)
+
+// GPGVersion enum representing the GPG client version to use.
+type GPGVersion int
+
+const (
+ // GPGv2 signifies gpgv2+
+ GPGv2 GPGVersion = iota
+ // GPGv1 signifies gpgv1+
+ GPGv1
+ // GPGVersionUndetermined signifies gpg client version undetermined
+ GPGVersionUndetermined
+)
+
+// GPGClient defines an interface for wrapping the gpg command line tools
+type GPGClient interface {
+ // ReadGPGPubRingFile gets the byte sequence of the gpg public keyring
+ ReadGPGPubRingFile() ([]byte, error)
+ // GetGPGPrivateKey gets the private key bytes of a keyid given a passphrase
+ GetGPGPrivateKey(keyid uint64, passphrase string) ([]byte, error)
+ // GetSecretKeyDetails gets the details of a secret key
+ GetSecretKeyDetails(keyid uint64) ([]byte, bool, error)
+ // GetKeyDetails gets the details of a public key
+ GetKeyDetails(keyid uint64) ([]byte, bool, error)
+ // ResolveRecipients resolves PGP key ids to user names
+ ResolveRecipients([]string) []string
+}
+
+// gpgClient contains generic gpg client information
+type gpgClient struct {
+ gpgHomeDir string
+}
+
+// gpgv2Client is a gpg2 client
+type gpgv2Client struct {
+ gpgClient
+}
+
+// gpgv1Client is a gpg client
+type gpgv1Client struct {
+ gpgClient
+}
+
+// GuessGPGVersion guesses the version of gpg. Defaults to gpg2 if exists, if
+// not defaults to regular gpg.
+func GuessGPGVersion() GPGVersion {
+ if err := exec.Command("gpg2", "--version").Run(); err == nil {
+ return GPGv2
+ } else if err := exec.Command("gpg", "--version").Run(); err == nil {
+ return GPGv1
+ } else {
+ return GPGVersionUndetermined
+ }
+}
+
+// NewGPGClient creates a new GPGClient object representing the given version
+// and using the given home directory
+func NewGPGClient(gpgVersion, gpgHomeDir string) (GPGClient, error) {
+ v := new(GPGVersion)
+ switch gpgVersion {
+ case "v1":
+ *v = GPGv1
+ case "v2":
+ *v = GPGv2
+ default:
+ v = nil
+ }
+ return newGPGClient(v, gpgHomeDir)
+}
+
+func newGPGClient(version *GPGVersion, homedir string) (GPGClient, error) {
+ var gpgVersion GPGVersion
+ if version != nil {
+ gpgVersion = *version
+ } else {
+ gpgVersion = GuessGPGVersion()
+ }
+
+ switch gpgVersion {
+ case GPGv1:
+ return &gpgv1Client{
+ gpgClient: gpgClient{gpgHomeDir: homedir},
+ }, nil
+ case GPGv2:
+ return &gpgv2Client{
+ gpgClient: gpgClient{gpgHomeDir: homedir},
+ }, nil
+ case GPGVersionUndetermined:
+ return nil, fmt.Errorf("unable to determine GPG version")
+ default:
+ return nil, fmt.Errorf("unhandled case: NewGPGClient")
+ }
+}
+
+// GetGPGPrivateKey gets the bytes of a specified keyid, supplying a passphrase
+func (gc *gpgv2Client) GetGPGPrivateKey(keyid uint64, passphrase string) ([]byte, error) {
+ var args []string
+
+ if gc.gpgHomeDir != "" {
+ args = append(args, []string{"--homedir", gc.gpgHomeDir}...)
+ }
+
+ rfile, wfile, err := os.Pipe()
+ if err != nil {
+ return nil, fmt.Errorf("could not create pipe: %w", err)
+ }
+ defer func() {
+ rfile.Close()
+ wfile.Close()
+ }()
+ // fill pipe in background
+ go func(passphrase string) {
+ _, _ = wfile.Write([]byte(passphrase))
+ wfile.Close()
+ }(passphrase)
+
+ args = append(args, []string{"--pinentry-mode", "loopback", "--batch", "--passphrase-fd", fmt.Sprintf("%d", 3), "--export-secret-key", fmt.Sprintf("0x%x", keyid)}...)
+
+ cmd := exec.Command("gpg2", args...)
+ cmd.ExtraFiles = []*os.File{rfile}
+
+ return runGPGGetOutput(cmd)
+}
+
+// ReadGPGPubRingFile reads the GPG public key ring file
+func (gc *gpgv2Client) ReadGPGPubRingFile() ([]byte, error) {
+ var args []string
+
+ if gc.gpgHomeDir != "" {
+ args = append(args, []string{"--homedir", gc.gpgHomeDir}...)
+ }
+ args = append(args, []string{"--batch", "--export"}...)
+
+ cmd := exec.Command("gpg2", args...)
+
+ return runGPGGetOutput(cmd)
+}
+
+func (gc *gpgv2Client) getKeyDetails(option string, keyid uint64) ([]byte, bool, error) {
+ var args []string
+
+ if gc.gpgHomeDir != "" {
+ args = []string{"--homedir", gc.gpgHomeDir}
+ }
+ args = append(args, option, fmt.Sprintf("0x%x", keyid))
+
+ cmd := exec.Command("gpg2", args...)
+
+ keydata, err := runGPGGetOutput(cmd)
+ return keydata, err == nil, err
+}
+
+// GetSecretKeyDetails retrieves the secret key details of key with keyid.
+// returns a byte array of the details and a bool if the key exists
+func (gc *gpgv2Client) GetSecretKeyDetails(keyid uint64) ([]byte, bool, error) {
+ return gc.getKeyDetails("-K", keyid)
+}
+
+// GetKeyDetails retrieves the public key details of key with keyid.
+// returns a byte array of the details and a bool if the key exists
+func (gc *gpgv2Client) GetKeyDetails(keyid uint64) ([]byte, bool, error) {
+ return gc.getKeyDetails("-k", keyid)
+}
+
+// ResolveRecipients converts PGP keyids to email addresses, if possible
+func (gc *gpgv2Client) ResolveRecipients(recipients []string) []string {
+ return resolveRecipients(gc, recipients)
+}
+
+// GetGPGPrivateKey gets the bytes of a specified keyid, supplying a passphrase
+func (gc *gpgv1Client) GetGPGPrivateKey(keyid uint64, _ string) ([]byte, error) {
+ var args []string
+
+ if gc.gpgHomeDir != "" {
+ args = append(args, []string{"--homedir", gc.gpgHomeDir}...)
+ }
+ args = append(args, []string{"--batch", "--export-secret-key", fmt.Sprintf("0x%x", keyid)}...)
+
+ cmd := exec.Command("gpg", args...)
+
+ return runGPGGetOutput(cmd)
+}
+
+// ReadGPGPubRingFile reads the GPG public key ring file
+func (gc *gpgv1Client) ReadGPGPubRingFile() ([]byte, error) {
+ var args []string
+
+ if gc.gpgHomeDir != "" {
+ args = append(args, []string{"--homedir", gc.gpgHomeDir}...)
+ }
+ args = append(args, []string{"--batch", "--export"}...)
+
+ cmd := exec.Command("gpg", args...)
+
+ return runGPGGetOutput(cmd)
+}
+
+func (gc *gpgv1Client) getKeyDetails(option string, keyid uint64) ([]byte, bool, error) {
+ var args []string
+
+ if gc.gpgHomeDir != "" {
+ args = []string{"--homedir", gc.gpgHomeDir}
+ }
+ args = append(args, option, fmt.Sprintf("0x%x", keyid))
+
+ cmd := exec.Command("gpg", args...)
+
+ keydata, err := runGPGGetOutput(cmd)
+
+ return keydata, err == nil, err
+}
+
+// GetSecretKeyDetails retrieves the secret key details of key with keyid.
+// returns a byte array of the details and a bool if the key exists
+func (gc *gpgv1Client) GetSecretKeyDetails(keyid uint64) ([]byte, bool, error) {
+ return gc.getKeyDetails("-K", keyid)
+}
+
+// GetKeyDetails retrieves the public key details of key with keyid.
+// returns a byte array of the details and a bool if the key exists
+func (gc *gpgv1Client) GetKeyDetails(keyid uint64) ([]byte, bool, error) {
+ return gc.getKeyDetails("-k", keyid)
+}
+
+// ResolveRecipients converts PGP keyids to email addresses, if possible
+func (gc *gpgv1Client) ResolveRecipients(recipients []string) []string {
+ return resolveRecipients(gc, recipients)
+}
+
+// runGPGGetOutput runs the GPG commandline and returns stdout as byte array
+// and any stderr in the error
+func runGPGGetOutput(cmd *exec.Cmd) ([]byte, error) {
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return nil, err
+ }
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ return nil, err
+ }
+ if err := cmd.Start(); err != nil {
+ return nil, err
+ }
+
+ stdoutstr, err2 := io.ReadAll(stdout)
+ stderrstr, _ := io.ReadAll(stderr)
+
+ if err := cmd.Wait(); err != nil {
+ return nil, fmt.Errorf("error from %s: %s", cmd.Path, string(stderrstr))
+ }
+
+ return stdoutstr, err2
+}
+
+// resolveRecipients walks the list of recipients and attempts to convert
+// all keyIds to email addresses; if something goes wrong during the
+// conversion of a recipient, the original string is returned for that
+// recpient
+func resolveRecipients(gc GPGClient, recipients []string) []string {
+ var result []string
+
+ for _, recipient := range recipients {
+ keyID, err := strconv.ParseUint(recipient, 0, 64)
+ if err != nil {
+ result = append(result, recipient)
+ } else {
+ details, found, _ := gc.GetKeyDetails(keyID)
+ if !found {
+ result = append(result, recipient)
+ } else {
+ email := extractEmailFromDetails(details)
+ if email == "" {
+ result = append(result, recipient)
+ } else {
+ result = append(result, email)
+ }
+ }
+ }
+ }
+ return result
+}
+
+var (
+ onceRegexp sync.Once
+ emailPattern *regexp.Regexp
+)
+
+func extractEmailFromDetails(details []byte) string {
+ onceRegexp.Do(func() {
+ emailPattern = regexp.MustCompile(`uid\s+\[.*\]\s.*\s<(?P<email>.+)>`)
+ })
+ loc := emailPattern.FindSubmatchIndex(details)
+ if len(loc) == 0 {
+ return ""
+ }
+ return string(emailPattern.Expand(nil, []byte("$email"), details, loc))
+}
+
+// uint64ToStringArray converts an array of uint64's to an array of strings
+// by applying a format string to each uint64
+func uint64ToStringArray(format string, in []uint64) []string {
+ var ret []string
+
+ for _, v := range in {
+ ret = append(ret, fmt.Sprintf(format, v))
+ }
+ return ret
+}
+
+// GPGGetPrivateKey walks the list of layerInfos and tries to decrypt the
+// wrapped symmetric keys. For this it determines whether a private key is
+// in the GPGVault or on this system and prompts for the passwords for those
+// that are available. If we do not find a private key on the system for
+// getting to the symmetric key of a layer then an error is generated.
+func GPGGetPrivateKey(descs []ocispec.Descriptor, gpgClient GPGClient, gpgVault GPGVault, mustFindKey bool) (gpgPrivKeys [][]byte, gpgPrivKeysPwds [][]byte, err error) {
+ // PrivateKeyData describes a private key
+ type PrivateKeyData struct {
+ KeyData []byte
+ KeyDataPassword []byte
+ }
+ var pkd PrivateKeyData
+ keyIDPasswordMap := make(map[uint64]PrivateKeyData)
+
+ for _, desc := range descs {
+ for scheme, b64pgpPackets := range GetWrappedKeysMap(desc) {
+ if scheme != "pgp" {
+ continue
+ }
+ keywrapper := GetKeyWrapper(scheme)
+ if keywrapper == nil {
+ return nil, nil, fmt.Errorf("could not get KeyWrapper for %s", scheme)
+ }
+ keyIds, err := keywrapper.GetKeyIdsFromPacket(b64pgpPackets)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ found := false
+ for _, keyid := range keyIds {
+ // do we have this key? -- first check the vault
+ if gpgVault != nil {
+ _, keydata := gpgVault.GetGPGPrivateKey(keyid)
+ if len(keydata) > 0 {
+ pkd = PrivateKeyData{
+ KeyData: keydata,
+ KeyDataPassword: nil, // password not supported in this case
+ }
+ keyIDPasswordMap[keyid] = pkd
+ found = true
+ break
+ }
+ } else if gpgClient != nil {
+ // check the local system's gpg installation
+ keyinfo, haveKey, _ := gpgClient.GetSecretKeyDetails(keyid)
+ // this may fail if the key is not here; we ignore the error
+ if !haveKey {
+ // key not on this system
+ continue
+ }
+
+ _, found = keyIDPasswordMap[keyid]
+ if !found {
+ fmt.Printf("Passphrase required for Key id 0x%x: \n%v", keyid, string(keyinfo))
+ fmt.Printf("Enter passphrase for key with Id 0x%x: ", keyid)
+
+ password, err := term.ReadPassword(int(os.Stdin.Fd()))
+ fmt.Printf("\n")
+ if err != nil {
+ return nil, nil, err
+ }
+ keydata, err := gpgClient.GetGPGPrivateKey(keyid, string(password))
+ if err != nil {
+ return nil, nil, err
+ }
+ pkd = PrivateKeyData{
+ KeyData: keydata,
+ KeyDataPassword: password,
+ }
+ keyIDPasswordMap[keyid] = pkd
+ found = true
+ }
+ break
+ } else {
+ return nil, nil, errors.New("no GPGVault or GPGClient passed")
+ }
+ }
+ if !found && len(b64pgpPackets) > 0 && mustFindKey {
+ ids := uint64ToStringArray("0x%x", keyIds)
+
+ return nil, nil, fmt.Errorf("missing key for decryption of layer %x of %s. Need one of the following keys: %s", desc.Digest, desc.Platform, strings.Join(ids, ", "))
+ }
+ }
+ }
+
+ for _, pkd := range keyIDPasswordMap {
+ gpgPrivKeys = append(gpgPrivKeys, pkd.KeyData)
+ gpgPrivKeysPwds = append(gpgPrivKeysPwds, pkd.KeyDataPassword)
+ }
+
+ return gpgPrivKeys, gpgPrivKeysPwds, nil
+}
diff --git a/gpgvault.go b/gpgvault.go
new file mode 100644
index 0000000..f1bd0d9
--- /dev/null
+++ b/gpgvault.go
@@ -0,0 +1,100 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ocicrypt
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+
+ "golang.org/x/crypto/openpgp"
+ "golang.org/x/crypto/openpgp/packet"
+)
+
+// GPGVault defines an interface for wrapping multiple secret key rings
+type GPGVault interface {
+ // AddSecretKeyRingData adds a secret keyring via its raw byte array
+ AddSecretKeyRingData(gpgSecretKeyRingData []byte) error
+ // AddSecretKeyRingDataArray adds secret keyring via its raw byte arrays
+ AddSecretKeyRingDataArray(gpgSecretKeyRingDataArray [][]byte) error
+ // AddSecretKeyRingFiles adds secret keyrings given their filenames
+ AddSecretKeyRingFiles(filenames []string) error
+ // GetGPGPrivateKey gets the private key bytes of a keyid given a passphrase
+ GetGPGPrivateKey(keyid uint64) ([]openpgp.Key, []byte)
+}
+
+// gpgVault wraps an array of gpgSecretKeyRing
+type gpgVault struct {
+ entityLists []openpgp.EntityList
+ keyDataList [][]byte // the raw data original passed in
+}
+
+// NewGPGVault creates an empty GPGVault
+func NewGPGVault() GPGVault {
+ return &gpgVault{}
+}
+
+// AddSecretKeyRingData adds a secret keyring's to the gpgVault; the raw byte
+// array read from the file must be passed and will be parsed by this function
+func (g *gpgVault) AddSecretKeyRingData(gpgSecretKeyRingData []byte) error {
+ // read the private keys
+ r := bytes.NewReader(gpgSecretKeyRingData)
+ entityList, err := openpgp.ReadKeyRing(r)
+ if err != nil {
+ return fmt.Errorf("could not read keyring: %w", err)
+ }
+ g.entityLists = append(g.entityLists, entityList)
+ g.keyDataList = append(g.keyDataList, gpgSecretKeyRingData)
+ return nil
+}
+
+// AddSecretKeyRingDataArray adds secret keyrings to the gpgVault; the raw byte
+// arrays read from files must be passed
+func (g *gpgVault) AddSecretKeyRingDataArray(gpgSecretKeyRingDataArray [][]byte) error {
+ for _, gpgSecretKeyRingData := range gpgSecretKeyRingDataArray {
+ if err := g.AddSecretKeyRingData(gpgSecretKeyRingData); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// AddSecretKeyRingFiles adds the secret key rings given their filenames
+func (g *gpgVault) AddSecretKeyRingFiles(filenames []string) error {
+ for _, filename := range filenames {
+ gpgSecretKeyRingData, err := os.ReadFile(filename)
+ if err != nil {
+ return err
+ }
+ err = g.AddSecretKeyRingData(gpgSecretKeyRingData)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// GetGPGPrivateKey gets the bytes of a specified keyid, supplying a passphrase
+func (g *gpgVault) GetGPGPrivateKey(keyid uint64) ([]openpgp.Key, []byte) {
+ for i, el := range g.entityLists {
+ decKeys := el.KeysByIdUsage(keyid, packet.KeyFlagEncryptCommunications)
+ if len(decKeys) > 0 {
+ return decKeys, g.keyDataList[i]
+ }
+ }
+ return nil, nil
+}
diff --git a/helpers/parse_helpers.go b/helpers/parse_helpers.go
new file mode 100644
index 0000000..18f4fa9
--- /dev/null
+++ b/helpers/parse_helpers.go
@@ -0,0 +1,377 @@
+package helpers
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+
+ "github.com/containers/ocicrypt"
+ encconfig "github.com/containers/ocicrypt/config"
+ "github.com/containers/ocicrypt/config/pkcs11config"
+ "github.com/containers/ocicrypt/crypto/pkcs11"
+ encutils "github.com/containers/ocicrypt/utils"
+)
+
+// processRecipientKeys sorts the array of recipients by type. Recipients may be either
+// x509 certificates, public keys, or PGP public keys identified by email address or name
+func processRecipientKeys(recipients []string) ([][]byte, [][]byte, [][]byte, [][]byte, [][]byte, [][]byte, error) {
+ var (
+ gpgRecipients [][]byte
+ pubkeys [][]byte
+ x509s [][]byte
+ pkcs11Pubkeys [][]byte
+ pkcs11Yamls [][]byte
+ keyProviders [][]byte
+ )
+
+ for _, recipient := range recipients {
+
+ idx := strings.Index(recipient, ":")
+ if idx < 0 {
+ return nil, nil, nil, nil, nil, nil, errors.New("Invalid recipient format")
+ }
+
+ protocol := recipient[:idx]
+ value := recipient[idx+1:]
+
+ switch protocol {
+ case "pgp":
+ gpgRecipients = append(gpgRecipients, []byte(value))
+
+ case "jwe":
+ tmp, err := os.ReadFile(value)
+ if err != nil {
+ return nil, nil, nil, nil, nil, nil, fmt.Errorf("Unable to read file: %w", err)
+ }
+ if !encutils.IsPublicKey(tmp) {
+ return nil, nil, nil, nil, nil, nil, errors.New("File provided is not a public key")
+ }
+ pubkeys = append(pubkeys, tmp)
+
+ case "pkcs7":
+ tmp, err := os.ReadFile(value)
+ if err != nil {
+ return nil, nil, nil, nil, nil, nil, fmt.Errorf("Unable to read file: %w", err)
+ }
+ if !encutils.IsCertificate(tmp) {
+ return nil, nil, nil, nil, nil, nil, errors.New("File provided is not an x509 cert")
+ }
+ x509s = append(x509s, tmp)
+
+ case "pkcs11":
+ tmp, err := os.ReadFile(value)
+ if err != nil {
+ return nil, nil, nil, nil, nil, nil, fmt.Errorf("Unable to read file: %w", err)
+ }
+ if encutils.IsPkcs11PublicKey(tmp) {
+ pkcs11Yamls = append(pkcs11Yamls, tmp)
+ } else if encutils.IsPublicKey(tmp) {
+ pkcs11Pubkeys = append(pkcs11Pubkeys, tmp)
+ } else {
+ return nil, nil, nil, nil, nil, nil, errors.New("Provided file is not a public key")
+ }
+
+ case "provider":
+ keyProviders = append(keyProviders, []byte(value))
+
+ default:
+ return nil, nil, nil, nil, nil, nil, errors.New("Provided protocol not recognized")
+ }
+ }
+ return gpgRecipients, pubkeys, x509s, pkcs11Pubkeys, pkcs11Yamls, keyProviders, nil
+}
+
+// processx509Certs processes x509 certificate files
+func processx509Certs(keys []string) ([][]byte, error) {
+ var x509s [][]byte
+ for _, key := range keys {
+ fileName := strings.Split(key, ":")[0]
+ if _, err := os.Stat(fileName); os.IsNotExist(err) {
+ continue
+ }
+ tmp, err := os.ReadFile(fileName)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to read file: %w", err)
+ }
+ if !encutils.IsCertificate(tmp) {
+ continue
+ }
+ x509s = append(x509s, tmp)
+
+ }
+ return x509s, nil
+}
+
+// processPwdString process a password that may be in any of the following formats:
+// - file=<passwordfile>
+// - pass=<password>
+// - fd=<filedescriptor>
+// - <password>
+func processPwdString(pwdString string) ([]byte, error) {
+ if strings.HasPrefix(pwdString, "file=") {
+ return os.ReadFile(pwdString[5:])
+ } else if strings.HasPrefix(pwdString, "pass=") {
+ return []byte(pwdString[5:]), nil
+ } else if strings.HasPrefix(pwdString, "fd=") {
+ fdStr := pwdString[3:]
+ fd, err := strconv.Atoi(fdStr)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse file descriptor %s: %w", fdStr, err)
+ }
+ f := os.NewFile(uintptr(fd), "pwdfile")
+ if f == nil {
+ return nil, fmt.Errorf("%s is not a valid file descriptor", fdStr)
+ }
+ defer f.Close()
+ pwd := make([]byte, 64)
+ n, err := f.Read(pwd)
+ if err != nil {
+ return nil, fmt.Errorf("could not read from file descriptor: %w", err)
+ }
+ return pwd[:n], nil
+ }
+ return []byte(pwdString), nil
+}
+
+// processPrivateKeyFiles sorts the different types of private key files; private key files may either be
+// private keys or GPG private key ring files. The private key files may include the password for the
+// private key and take any of the following forms:
+// - <filename>
+// - <filename>:file=<passwordfile>
+// - <filename>:pass=<password>
+// - <filename>:fd=<filedescriptor>
+// - <filename>:<password>
+// - keyprovider:<...>
+func processPrivateKeyFiles(keyFilesAndPwds []string) ([][]byte, [][]byte, [][]byte, [][]byte, [][]byte, [][]byte, error) {
+ var (
+ gpgSecretKeyRingFiles [][]byte
+ gpgSecretKeyPasswords [][]byte
+ privkeys [][]byte
+ privkeysPasswords [][]byte
+ pkcs11Yamls [][]byte
+ keyProviders [][]byte
+ err error
+ )
+ // keys needed for decryption in case of adding a recipient
+ for _, keyfileAndPwd := range keyFilesAndPwds {
+ var password []byte
+
+ // treat "provider" protocol separately
+ if strings.HasPrefix(keyfileAndPwd, "provider:") {
+ keyProviders = append(keyProviders, []byte(keyfileAndPwd[len("provider:"):]))
+ continue
+ }
+ parts := strings.Split(keyfileAndPwd, ":")
+ if len(parts) == 2 {
+ password, err = processPwdString(parts[1])
+ if err != nil {
+ return nil, nil, nil, nil, nil, nil, err
+ }
+ }
+
+ keyfile := parts[0]
+ tmp, err := os.ReadFile(keyfile)
+ if err != nil {
+ return nil, nil, nil, nil, nil, nil, err
+ }
+ isPrivKey, err := encutils.IsPrivateKey(tmp, password)
+ if encutils.IsPasswordError(err) {
+ return nil, nil, nil, nil, nil, nil, err
+ }
+
+ if encutils.IsPkcs11PrivateKey(tmp) {
+ pkcs11Yamls = append(pkcs11Yamls, tmp)
+ } else if isPrivKey {
+ privkeys = append(privkeys, tmp)
+ privkeysPasswords = append(privkeysPasswords, password)
+ } else if encutils.IsGPGPrivateKeyRing(tmp) {
+ gpgSecretKeyRingFiles = append(gpgSecretKeyRingFiles, tmp)
+ gpgSecretKeyPasswords = append(gpgSecretKeyPasswords, password)
+ } else {
+ // ignore if file is not recognized, so as not to error if additional
+ // metadata/cert files exists
+ continue
+ }
+ }
+ return gpgSecretKeyRingFiles, gpgSecretKeyPasswords, privkeys, privkeysPasswords, pkcs11Yamls, keyProviders, nil
+}
+
+// CreateDecryptCryptoConfig creates the CryptoConfig object that contains the necessary
+// information to perform decryption from command line options.
+func CreateDecryptCryptoConfig(keys []string, decRecipients []string) (encconfig.CryptoConfig, error) {
+ ccs := []encconfig.CryptoConfig{}
+
+ // x509 cert is needed for PKCS7 decryption
+ _, _, x509s, _, _, _, err := processRecipientKeys(decRecipients)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+
+ // x509 certs can also be passed in via keys
+ x509FromKeys, err := processx509Certs(keys)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ x509s = append(x509s, x509FromKeys...)
+
+ gpgSecretKeyRingFiles, gpgSecretKeyPasswords, privKeys, privKeysPasswords, pkcs11Yamls, keyProviders, err := processPrivateKeyFiles(keys)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+
+ if len(gpgSecretKeyRingFiles) > 0 {
+ gpgCc, err := encconfig.DecryptWithGpgPrivKeys(gpgSecretKeyRingFiles, gpgSecretKeyPasswords)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ ccs = append(ccs, gpgCc)
+ }
+
+ /* TODO: Add in GPG client query for secret keys in the future.
+ _, err = createGPGClient(context)
+ gpgInstalled := err == nil
+ if gpgInstalled {
+ if len(gpgSecretKeyRingFiles) == 0 && len(privKeys) == 0 && len(pkcs11Yamls) == 0 && len(keyProviders) == 0 && descs != nil {
+ // Get pgp private keys from keyring only if no private key was passed
+ gpgPrivKeys, gpgPrivKeyPasswords, err := getGPGPrivateKeys(context, gpgSecretKeyRingFiles, descs, true)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+
+ gpgCc, err := encconfig.DecryptWithGpgPrivKeys(gpgPrivKeys, gpgPrivKeyPasswords)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ ccs = append(ccs, gpgCc)
+
+ } else if len(gpgSecretKeyRingFiles) > 0 {
+ gpgCc, err := encconfig.DecryptWithGpgPrivKeys(gpgSecretKeyRingFiles, gpgSecretKeyPasswords)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ ccs = append(ccs, gpgCc)
+
+ }
+ }
+ */
+
+ if len(x509s) > 0 {
+ x509sCc, err := encconfig.DecryptWithX509s(x509s)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ ccs = append(ccs, x509sCc)
+ }
+ if len(privKeys) > 0 {
+ privKeysCc, err := encconfig.DecryptWithPrivKeys(privKeys, privKeysPasswords)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ ccs = append(ccs, privKeysCc)
+ }
+ if len(pkcs11Yamls) > 0 {
+ p11conf, err := pkcs11config.GetUserPkcs11Config()
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ pkcs11PrivKeysCc, err := encconfig.DecryptWithPkcs11Yaml(p11conf, pkcs11Yamls)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ ccs = append(ccs, pkcs11PrivKeysCc)
+ }
+ if len(keyProviders) > 0 {
+ keyProviderCc, err := encconfig.DecryptWithKeyProvider(keyProviders)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ ccs = append(ccs, keyProviderCc)
+ }
+ return encconfig.CombineCryptoConfigs(ccs), nil
+}
+
+// CreateCryptoConfig from the list of recipient strings and list of key paths of private keys
+func CreateCryptoConfig(recipients []string, keys []string) (encconfig.CryptoConfig, error) {
+ var decryptCc *encconfig.CryptoConfig
+ ccs := []encconfig.CryptoConfig{}
+ if len(keys) > 0 {
+ dcc, err := CreateDecryptCryptoConfig(keys, []string{})
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ decryptCc = &dcc
+ ccs = append(ccs, dcc)
+ }
+
+ if len(recipients) > 0 {
+ gpgRecipients, pubKeys, x509s, pkcs11Pubkeys, pkcs11Yamls, keyProvider, err := processRecipientKeys(recipients)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ encryptCcs := []encconfig.CryptoConfig{}
+
+ // Create GPG client with guessed GPG version and default homedir
+ gpgClient, err := ocicrypt.NewGPGClient("", "")
+ gpgInstalled := err == nil
+ if len(gpgRecipients) > 0 && gpgInstalled {
+ gpgPubRingFile, err := gpgClient.ReadGPGPubRingFile()
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+
+ gpgCc, err := encconfig.EncryptWithGpg(gpgRecipients, gpgPubRingFile)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ encryptCcs = append(encryptCcs, gpgCc)
+ }
+
+ // Create Encryption Crypto Config
+ if len(x509s) > 0 {
+ pkcs7Cc, err := encconfig.EncryptWithPkcs7(x509s)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ encryptCcs = append(encryptCcs, pkcs7Cc)
+ }
+ if len(pubKeys) > 0 {
+ jweCc, err := encconfig.EncryptWithJwe(pubKeys)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ encryptCcs = append(encryptCcs, jweCc)
+ }
+ var p11conf *pkcs11.Pkcs11Config
+ if len(pkcs11Yamls) > 0 || len(pkcs11Pubkeys) > 0 {
+ p11conf, err = pkcs11config.GetUserPkcs11Config()
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ pkcs11Cc, err := encconfig.EncryptWithPkcs11(p11conf, pkcs11Pubkeys, pkcs11Yamls)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ encryptCcs = append(encryptCcs, pkcs11Cc)
+ }
+
+ if len(keyProvider) > 0 {
+ keyProviderCc, err := encconfig.EncryptWithKeyProvider(keyProvider)
+ if err != nil {
+ return encconfig.CryptoConfig{}, err
+ }
+ encryptCcs = append(encryptCcs, keyProviderCc)
+ }
+ ecc := encconfig.CombineCryptoConfigs(encryptCcs)
+ if decryptCc != nil {
+ ecc.EncryptConfig.AttachDecryptConfig(decryptCc.DecryptConfig)
+ }
+ ccs = append(ccs, ecc)
+ }
+
+ if len(ccs) > 0 {
+ return encconfig.CombineCryptoConfigs(ccs), nil
+ }
+ return encconfig.CryptoConfig{}, nil
+}
diff --git a/keywrap/jwe/keywrapper_jwe.go b/keywrap/jwe/keywrapper_jwe.go
new file mode 100644
index 0000000..cd2241c
--- /dev/null
+++ b/keywrap/jwe/keywrapper_jwe.go
@@ -0,0 +1,137 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package jwe
+
+import (
+ "crypto/ecdsa"
+ "errors"
+ "fmt"
+
+ "github.com/containers/ocicrypt/config"
+ "github.com/containers/ocicrypt/keywrap"
+ "github.com/containers/ocicrypt/utils"
+ "github.com/go-jose/go-jose/v3"
+)
+
+type jweKeyWrapper struct {
+}
+
+func (kw *jweKeyWrapper) GetAnnotationID() string {
+ return "org.opencontainers.image.enc.keys.jwe"
+}
+
+// NewKeyWrapper returns a new key wrapping interface using jwe
+func NewKeyWrapper() keywrap.KeyWrapper {
+ return &jweKeyWrapper{}
+}
+
+// WrapKeys wraps the session key for recpients and encrypts the optsData, which
+// describe the symmetric key used for encrypting the layer
+func (kw *jweKeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) {
+ var joseRecipients []jose.Recipient
+
+ err := addPubKeys(&joseRecipients, ec.Parameters["pubkeys"])
+ if err != nil {
+ return nil, err
+ }
+ // no recipients is not an error...
+ if len(joseRecipients) == 0 {
+ return nil, nil
+ }
+
+ encrypter, err := jose.NewMultiEncrypter(jose.A256GCM, joseRecipients, nil)
+ if err != nil {
+ return nil, fmt.Errorf("jose.NewMultiEncrypter failed: %w", err)
+ }
+ jwe, err := encrypter.Encrypt(optsData)
+ if err != nil {
+ return nil, fmt.Errorf("JWE Encrypt failed: %w", err)
+ }
+ return []byte(jwe.FullSerialize()), nil
+}
+
+func (kw *jweKeyWrapper) UnwrapKey(dc *config.DecryptConfig, jweString []byte) ([]byte, error) {
+ jwe, err := jose.ParseEncrypted(string(jweString))
+ if err != nil {
+ return nil, errors.New("jose.ParseEncrypted failed")
+ }
+
+ privKeys := kw.GetPrivateKeys(dc.Parameters)
+ if len(privKeys) == 0 {
+ return nil, errors.New("No private keys found for JWE decryption")
+ }
+ privKeysPasswords := kw.getPrivateKeysPasswords(dc.Parameters)
+ if len(privKeysPasswords) != len(privKeys) {
+ return nil, errors.New("Private key password array length must be same as that of private keys")
+ }
+
+ for idx, privKey := range privKeys {
+ key, err := utils.ParsePrivateKey(privKey, privKeysPasswords[idx], "JWE")
+ if err != nil {
+ return nil, err
+ }
+ _, _, plain, err := jwe.DecryptMulti(key)
+ if err == nil {
+ return plain, nil
+ }
+ }
+ return nil, errors.New("JWE: No suitable private key found for decryption")
+}
+
+func (kw *jweKeyWrapper) NoPossibleKeys(dcparameters map[string][][]byte) bool {
+ return len(kw.GetPrivateKeys(dcparameters)) == 0
+}
+
+func (kw *jweKeyWrapper) GetPrivateKeys(dcparameters map[string][][]byte) [][]byte {
+ return dcparameters["privkeys"]
+}
+
+func (kw *jweKeyWrapper) getPrivateKeysPasswords(dcparameters map[string][][]byte) [][]byte {
+ return dcparameters["privkeys-passwords"]
+}
+
+func (kw *jweKeyWrapper) GetKeyIdsFromPacket(b64jwes string) ([]uint64, error) {
+ return nil, nil
+}
+
+func (kw *jweKeyWrapper) GetRecipients(b64jwes string) ([]string, error) {
+ return []string{"[jwe]"}, nil
+}
+
+func addPubKeys(joseRecipients *[]jose.Recipient, pubKeys [][]byte) error {
+ if len(pubKeys) == 0 {
+ return nil
+ }
+ for _, pubKey := range pubKeys {
+ key, err := utils.ParsePublicKey(pubKey, "JWE")
+ if err != nil {
+ return err
+ }
+
+ alg := jose.RSA_OAEP
+ switch key.(type) {
+ case *ecdsa.PublicKey:
+ alg = jose.ECDH_ES_A256KW
+ }
+
+ *joseRecipients = append(*joseRecipients, jose.Recipient{
+ Algorithm: alg,
+ Key: key,
+ })
+ }
+ return nil
+}
diff --git a/keywrap/jwe/keywrapper_jwe_test.go b/keywrap/jwe/keywrapper_jwe_test.go
new file mode 100644
index 0000000..3beea39
--- /dev/null
+++ b/keywrap/jwe/keywrapper_jwe_test.go
@@ -0,0 +1,345 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package jwe
+
+import (
+ "crypto/elliptic"
+ "testing"
+
+ "github.com/containers/ocicrypt/config"
+ "github.com/containers/ocicrypt/utils"
+ "github.com/go-jose/go-jose/v3"
+)
+
+var oneEmpty []byte
+
+func createValidJweCcs() ([]*config.CryptoConfig, error) {
+
+ jwePubKeyPem, jwePrivKeyPem, err := utils.CreateRSATestKey(2048, oneEmpty, true)
+ if err != nil {
+ return nil, err
+ }
+
+ jwePubKey2Pem, jwePrivKey2Pem, err := utils.CreateRSATestKey(1024, oneEmpty, true)
+ if err != nil {
+ return nil, err
+ }
+
+ jwePrivKey3Password := []byte("password")
+ jwePubKey3Pem, jwePrivKey3PassPem, err := utils.CreateRSATestKey(2048, jwePrivKey3Password, true)
+ if err != nil {
+ return nil, err
+ }
+
+ jwePubKeyDer, jwePrivKeyDer, err := utils.CreateRSATestKey(2048, oneEmpty, false)
+ if err != nil {
+ return nil, err
+ }
+
+ jweEcPubKeyDer, jweEcPrivKeyDer, err := utils.CreateECDSATestKey(elliptic.P521())
+ if err != nil {
+ return nil, err
+ }
+
+ key, err := utils.CreateRSAKey(2048)
+ if err != nil {
+ return nil, err
+ }
+
+ jwePrivKeyJwk, err := jose.JSONWebKey{Key: key}.MarshalJSON()
+ if err != nil {
+ return nil, err
+ }
+
+ jwePubKeyJwk, err := jose.JSONWebKey{Key: &key.PublicKey}.MarshalJSON()
+ if err != nil {
+ return nil, err
+ }
+
+ validJweCcs := []*config.CryptoConfig{
+ // Key 1
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "pubkeys": {jwePubKeyPem},
+ },
+ DecryptConfig: config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jwePrivKeyPem},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jwePrivKeyPem},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+
+ // Key 2
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "pubkeys": {jwePubKey2Pem},
+ },
+ DecryptConfig: config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jwePrivKey2Pem},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jwePrivKey2Pem},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+
+ // Key 1 without enc private key
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "pubkeys": {jwePubKeyPem},
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jwePrivKeyPem},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+
+ // Key 2 without enc private key
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "pubkeys": {jwePubKey2Pem},
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jwePrivKey2Pem},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+
+ // Key 3 with enc private key
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "pubkeys": {jwePubKey3Pem},
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jwePrivKey3PassPem},
+ "privkeys-passwords": {jwePrivKey3Password},
+ },
+ },
+ },
+
+ // Key (DER format)
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "pubkeys": {jwePubKeyDer},
+ },
+ DecryptConfig: config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jwePrivKeyDer},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jwePrivKeyDer},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+ // Key (JWK format)
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "pubkeys": {jwePubKeyJwk},
+ },
+ DecryptConfig: config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jwePrivKeyJwk},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jwePrivKeyJwk},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+ // EC Key (DER format)
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "pubkeys": {jweEcPubKeyDer},
+ },
+ DecryptConfig: config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jweEcPrivKeyDer},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jweEcPrivKeyDer},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+ }
+ return validJweCcs, nil
+}
+
+func createInvalidJweCcs() ([]*config.CryptoConfig, error) {
+
+ jwePubKeyPem, _, err := utils.CreateRSATestKey(2048, oneEmpty, true)
+ if err != nil {
+ return nil, err
+ }
+
+ jwePubKey2Pem, _, err := utils.CreateRSATestKey(2048, oneEmpty, true)
+ if err != nil {
+ return nil, err
+ }
+
+ invalidJweCcs := []*config.CryptoConfig{
+ // Client key 1 public with client 2 private decrypt
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "pubkeys": {jwePubKeyPem},
+ },
+ },
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jwePubKey2Pem},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+
+ // Client key 1 public with no private key
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "pubkeys": {jwePubKeyPem},
+ },
+ },
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{},
+ },
+ },
+
+ // Invalid Client key 1 private key
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "pubkeys": {jwePubKeyPem},
+ },
+ },
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {jwePubKeyPem},
+ "privkeys-passwords": {oneEmpty},
+ },
+ },
+ },
+ }
+ return invalidJweCcs, nil
+}
+
+func TestKeyWrapJweSuccess(t *testing.T) {
+ validJweCcs, err := createValidJweCcs()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, cc := range validJweCcs {
+ kw := NewKeyWrapper()
+
+ data := []byte("This is some secret text")
+
+ wk, err := kw.WrapKeys(cc.EncryptConfig, data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(data) != string(ud) {
+ t.Fatal("Strings don't match")
+ }
+ }
+}
+
+func TestKeyWrapJweInvalid(t *testing.T) {
+ invalidJweCcs, err := createInvalidJweCcs()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, cc := range invalidJweCcs {
+ kw := NewKeyWrapper()
+
+ data := []byte("This is some secret text")
+
+ wk, err := kw.WrapKeys(cc.EncryptConfig, data)
+ if err != nil {
+ return
+ }
+
+ ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
+ if err != nil {
+ return
+ }
+
+ if string(data) != string(ud) {
+ return
+ }
+
+ t.Fatal("Successfully wrap for invalid crypto config")
+ }
+}
diff --git a/keywrap/keyprovider/keyprovider.go b/keywrap/keyprovider/keyprovider.go
new file mode 100644
index 0000000..ddb244a
--- /dev/null
+++ b/keywrap/keyprovider/keyprovider.go
@@ -0,0 +1,244 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package keyprovider
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/containers/ocicrypt/config"
+ keyproviderconfig "github.com/containers/ocicrypt/config/keyprovider-config"
+ "github.com/containers/ocicrypt/keywrap"
+ "github.com/containers/ocicrypt/utils"
+ keyproviderpb "github.com/containers/ocicrypt/utils/keyprovider"
+ log "github.com/sirupsen/logrus"
+ "google.golang.org/grpc"
+)
+
+type keyProviderKeyWrapper struct {
+ provider string
+ attrs keyproviderconfig.KeyProviderAttrs
+}
+
+func (kw *keyProviderKeyWrapper) GetAnnotationID() string {
+ return "org.opencontainers.image.enc.keys.provider." + kw.provider
+}
+
+// NewKeyWrapper returns a new key wrapping interface using keyprovider
+func NewKeyWrapper(p string, a keyproviderconfig.KeyProviderAttrs) keywrap.KeyWrapper {
+ return &keyProviderKeyWrapper{provider: p, attrs: a}
+}
+
+type KeyProviderKeyWrapProtocolOperation string
+
+var (
+ OpKeyWrap KeyProviderKeyWrapProtocolOperation = "keywrap"
+ OpKeyUnwrap KeyProviderKeyWrapProtocolOperation = "keyunwrap"
+)
+
+// KeyProviderKeyWrapProtocolInput defines the input to the key provider binary or grpc method.
+type KeyProviderKeyWrapProtocolInput struct {
+ // Operation is either "keywrap" or "keyunwrap"
+ Operation KeyProviderKeyWrapProtocolOperation `json:"op"`
+ // KeyWrapParams encodes the arguments to key wrap if operation is set to wrap
+ KeyWrapParams KeyWrapParams `json:"keywrapparams,omitempty"`
+ // KeyUnwrapParams encodes the arguments to key unwrap if operation is set to unwrap
+ KeyUnwrapParams KeyUnwrapParams `json:"keyunwrapparams,omitempty"`
+}
+
+// KeyProviderKeyWrapProtocolOutput defines the output of the key provider binary or grpc method.
+type KeyProviderKeyWrapProtocolOutput struct {
+ // KeyWrapResult encodes the results to key wrap if operation is to wrap
+ KeyWrapResults KeyWrapResults `json:"keywrapresults,omitempty"`
+ // KeyUnwrapResult encodes the result to key unwrap if operation is to unwrap
+ KeyUnwrapResults KeyUnwrapResults `json:"keyunwrapresults,omitempty"`
+}
+
+type KeyWrapParams struct {
+ Ec *config.EncryptConfig `json:"ec"`
+ OptsData []byte `json:"optsdata"`
+}
+
+type KeyUnwrapParams struct {
+ Dc *config.DecryptConfig `json:"dc"`
+ Annotation []byte `json:"annotation"`
+}
+
+type KeyUnwrapResults struct {
+ OptsData []byte `json:"optsdata"`
+}
+
+type KeyWrapResults struct {
+ Annotation []byte `json:"annotation"`
+}
+
+var runner utils.CommandExecuter
+
+func init() {
+ runner = utils.Runner{}
+}
+
+// WrapKeys calls appropriate binary executable/grpc server for wrapping the session key for recipients and gets encrypted optsData, which
+// describe the symmetric key used for encrypting the layer
+func (kw *keyProviderKeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) {
+
+ input, err := json.Marshal(KeyProviderKeyWrapProtocolInput{
+ Operation: OpKeyWrap,
+ KeyWrapParams: KeyWrapParams{
+ Ec: ec,
+ OptsData: optsData,
+ },
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ if _, ok := ec.Parameters[kw.provider]; ok {
+ if kw.attrs.Command != nil {
+ protocolOuput, err := getProviderCommandOutput(input, kw.attrs.Command)
+ if err != nil {
+ return nil, fmt.Errorf("error while retrieving keyprovider protocol command output: %w", err)
+ }
+ return protocolOuput.KeyWrapResults.Annotation, nil
+ } else if kw.attrs.Grpc != "" {
+ protocolOuput, err := getProviderGRPCOutput(input, kw.attrs.Grpc, OpKeyWrap)
+ if err != nil {
+ return nil, fmt.Errorf("error while retrieving keyprovider protocol grpc output: %w", err)
+ }
+
+ return protocolOuput.KeyWrapResults.Annotation, nil
+ } else {
+ return nil, errors.New("Unsupported keyprovider invocation. Supported invocation methods are grpc and cmd")
+ }
+ }
+
+ return nil, nil
+}
+
+// UnwrapKey calls appropriate binary executable/grpc server for unwrapping the session key based on the protocol given in annotation for recipients and gets decrypted optsData,
+// which describe the symmetric key used for decrypting the layer
+func (kw *keyProviderKeyWrapper) UnwrapKey(dc *config.DecryptConfig, jsonString []byte) ([]byte, error) {
+ input, err := json.Marshal(KeyProviderKeyWrapProtocolInput{
+ Operation: OpKeyUnwrap,
+ KeyUnwrapParams: KeyUnwrapParams{
+ Dc: dc,
+ Annotation: jsonString,
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if kw.attrs.Command != nil {
+ protocolOuput, err := getProviderCommandOutput(input, kw.attrs.Command)
+ if err != nil {
+ // If err is not nil, then ignore it and continue with rest of the given keyproviders
+ return nil, err
+ }
+
+ return protocolOuput.KeyUnwrapResults.OptsData, nil
+ } else if kw.attrs.Grpc != "" {
+ protocolOuput, err := getProviderGRPCOutput(input, kw.attrs.Grpc, OpKeyUnwrap)
+ if err != nil {
+ // If err is not nil, then ignore it and continue with rest of the given keyproviders
+ return nil, err
+ }
+
+ return protocolOuput.KeyUnwrapResults.OptsData, nil
+ } else {
+ return nil, errors.New("Unsupported keyprovider invocation. Supported invocation methods are grpc and cmd")
+ }
+}
+
+func getProviderGRPCOutput(input []byte, connString string, operation KeyProviderKeyWrapProtocolOperation) (*KeyProviderKeyWrapProtocolOutput, error) {
+ var protocolOuput KeyProviderKeyWrapProtocolOutput
+ var grpcOutput *keyproviderpb.KeyProviderKeyWrapProtocolOutput
+ cc, err := grpc.Dial(connString, grpc.WithInsecure())
+ if err != nil {
+ return nil, fmt.Errorf("error while dialing rpc server: %w", err)
+ }
+ defer func() {
+ derr := cc.Close()
+ if derr != nil {
+ log.WithError(derr).Error("Error closing grpc socket")
+ }
+ }()
+
+ client := keyproviderpb.NewKeyProviderServiceClient(cc)
+ req := &keyproviderpb.KeyProviderKeyWrapProtocolInput{
+ KeyProviderKeyWrapProtocolInput: input,
+ }
+
+ if operation == OpKeyWrap {
+ grpcOutput, err = client.WrapKey(context.Background(), req)
+ if err != nil {
+ return nil, fmt.Errorf("Error from grpc method: %w", err)
+ }
+ } else if operation == OpKeyUnwrap {
+ grpcOutput, err = client.UnWrapKey(context.Background(), req)
+ if err != nil {
+ return nil, fmt.Errorf("Error from grpc method: %w", err)
+ }
+ } else {
+ return nil, errors.New("Unsupported operation")
+ }
+
+ respBytes := grpcOutput.GetKeyProviderKeyWrapProtocolOutput()
+ err = json.Unmarshal(respBytes, &protocolOuput)
+ if err != nil {
+ return nil, fmt.Errorf("Error while unmarshalling grpc method output: %w", err)
+ }
+
+ return &protocolOuput, nil
+}
+
+func getProviderCommandOutput(input []byte, command *keyproviderconfig.Command) (*KeyProviderKeyWrapProtocolOutput, error) {
+ var protocolOuput KeyProviderKeyWrapProtocolOutput
+ // Convert interface to command structure
+ respBytes, err := runner.Exec(command.Path, command.Args, input)
+ if err != nil {
+ return nil, err
+ }
+ err = json.Unmarshal(respBytes, &protocolOuput)
+ if err != nil {
+ return nil, fmt.Errorf("Error while unmarshalling binary executable command output: %w", err)
+ }
+ return &protocolOuput, nil
+}
+
+// Return false as it is not applicable to keyprovider protocol
+func (kw *keyProviderKeyWrapper) NoPossibleKeys(dcparameters map[string][][]byte) bool {
+ return false
+}
+
+// Return nil as it is not applicable to keyprovider protocol
+func (kw *keyProviderKeyWrapper) GetPrivateKeys(dcparameters map[string][][]byte) [][]byte {
+ return nil
+}
+
+// Return nil as it is not applicable to keyprovider protocol
+func (kw *keyProviderKeyWrapper) GetKeyIdsFromPacket(_ string) ([]uint64, error) {
+ return nil, nil
+}
+
+// Return nil as it is not applicable to keyprovider protocol
+func (kw *keyProviderKeyWrapper) GetRecipients(_ string) ([]string, error) {
+ return nil, nil
+}
diff --git a/keywrap/keyprovider/keyprovider_test.go b/keywrap/keyprovider/keyprovider_test.go
new file mode 100644
index 0000000..8d66729
--- /dev/null
+++ b/keywrap/keyprovider/keyprovider_test.go
@@ -0,0 +1,378 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package keyprovider
+
+import (
+ "context"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "os"
+ "testing"
+
+ "github.com/containers/ocicrypt/config"
+ keyprovider_config "github.com/containers/ocicrypt/config/keyprovider-config"
+ keyproviderpb "github.com/containers/ocicrypt/utils/keyprovider"
+ "github.com/stretchr/testify/assert"
+ "google.golang.org/grpc"
+)
+
+// TestRunner mocks binary executable for key wrapping and unwrapping
+type TestRunner struct{}
+
+// Mock annotation packet, which goes into container image manifest
+type annotationPacket struct {
+ KeyUrl string `json:"key_url"`
+ WrappedKey []byte `json:"wrapped_key"`
+ WrapType string `json:"wrap_type"`
+}
+
+// grpc server with mock api implementation for serving the clients with mock WrapKey and Unwrapkey grpc method implementations
+type server struct {
+ keyproviderpb.UnimplementedKeyProviderServiceServer
+}
+
+var encryptingKey []byte
+var decryptingKey []byte
+
+func init() {
+ lis, _ := net.Listen("tcp", ":50051")
+ s := grpc.NewServer()
+ keyproviderpb.RegisterKeyProviderServiceServer(s, &server{})
+ go func() {
+ if err := s.Serve(lis); err != nil {
+ fmt.Println(err)
+ }
+ }()
+}
+
+// Mock grpc method which returns the wrapped key encapsulated in annotation packet in grpc response for a given key in grpc request
+func (*server) WrapKey(ctx context.Context, request *keyproviderpb.KeyProviderKeyWrapProtocolInput) (*keyproviderpb.KeyProviderKeyWrapProtocolOutput, error) {
+ var keyP KeyProviderKeyWrapProtocolInput
+ err := json.Unmarshal(request.KeyProviderKeyWrapProtocolInput, &keyP)
+ if err != nil {
+ return nil, err
+ }
+ c, _ := aes.NewCipher(encryptingKey)
+ gcm, _ := cipher.NewGCM(c)
+ nonce := make([]byte, gcm.NonceSize())
+ _, err = io.ReadFull(rand.Reader, nonce)
+ if err != nil {
+ return nil, err
+ }
+
+ wrappedKey := gcm.Seal(nonce, nonce, keyP.KeyWrapParams.OptsData, nil)
+
+ jsonString, _ := json.Marshal(annotationPacket{
+ KeyUrl: "https://key-provider/key-uuid",
+ WrappedKey: wrappedKey,
+ WrapType: "AES",
+ })
+
+ protocolOuputSerialized, _ := json.Marshal(KeyProviderKeyWrapProtocolOutput{
+ KeyWrapResults: KeyWrapResults{Annotation: jsonString},
+ })
+
+ return &keyproviderpb.KeyProviderKeyWrapProtocolOutput{
+ KeyProviderKeyWrapProtocolOutput: protocolOuputSerialized,
+ }, nil
+}
+
+// Mock grpc method which returns the unwrapped key encapsulated in grpc response for a given wrapped key encapsulated in annotation packet in grpc request
+func (*server) UnWrapKey(ctx context.Context, request *keyproviderpb.KeyProviderKeyWrapProtocolInput) (*keyproviderpb.KeyProviderKeyWrapProtocolOutput, error) {
+ var keyP KeyProviderKeyWrapProtocolInput
+ err := json.Unmarshal(request.KeyProviderKeyWrapProtocolInput, &keyP)
+ if err != nil {
+ return nil, err
+ }
+ apkt := annotationPacket{}
+ err = json.Unmarshal(keyP.KeyUnwrapParams.Annotation, &apkt)
+ if err != nil {
+ return nil, err
+ }
+ ciphertext := apkt.WrappedKey
+
+ c, _ := aes.NewCipher(decryptingKey)
+ gcm, _ := cipher.NewGCM(c)
+ nonceSize := gcm.NonceSize()
+ nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
+ unwrappedKey, err := gcm.Open(nil, nonce, ciphertext, nil)
+
+ if err != nil {
+ return nil, err
+ }
+
+ protocolOuputSerialized, _ := json.Marshal(KeyProviderKeyWrapProtocolOutput{
+ KeyUnwrapResults: KeyUnwrapResults{OptsData: unwrappedKey},
+ })
+ return &keyproviderpb.KeyProviderKeyWrapProtocolOutput{
+ KeyProviderKeyWrapProtocolOutput: protocolOuputSerialized,
+ }, nil
+}
+
+// Mock Exec Command for wrapping and unwrapping executables
+func (r TestRunner) Exec(cmdName string, args []string, input []byte) ([]byte, error) {
+ if cmdName == "/usr/lib/keyprovider-1-wrapkey" {
+ var keyP KeyProviderKeyWrapProtocolInput
+ err := json.Unmarshal(input, &keyP)
+ if err != nil {
+ return nil, err
+ }
+ c, _ := aes.NewCipher(encryptingKey)
+ gcm, _ := cipher.NewGCM(c)
+
+ nonce := make([]byte, gcm.NonceSize())
+ _, err = io.ReadFull(rand.Reader, nonce)
+ if err != nil {
+ return nil, err
+ }
+ wrappedKey := gcm.Seal(nonce, nonce, keyP.KeyWrapParams.OptsData, nil)
+
+ jsonString, _ := json.Marshal(annotationPacket{
+ KeyUrl: "https://key-provider/key-uuid",
+ WrappedKey: wrappedKey,
+ WrapType: "AES",
+ })
+
+ return json.Marshal(KeyProviderKeyWrapProtocolOutput{
+ KeyWrapResults: KeyWrapResults{
+ Annotation: jsonString,
+ },
+ })
+ } else if cmdName == "/usr/lib/keyprovider-1-unwrapkey" {
+ var keyP KeyProviderKeyWrapProtocolInput
+ err := json.Unmarshal(input, &keyP)
+ if err != nil {
+ return nil, err
+ }
+ apkt := annotationPacket{}
+ err = json.Unmarshal(keyP.KeyUnwrapParams.Annotation, &apkt)
+ if err != nil {
+ return nil, err
+ }
+ ciphertext := apkt.WrappedKey
+
+ c, _ := aes.NewCipher(decryptingKey)
+ gcm, _ := cipher.NewGCM(c)
+ nonceSize := gcm.NonceSize()
+ nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
+ unwrappedKey, err := gcm.Open(nil, nonce, ciphertext, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return json.Marshal(KeyProviderKeyWrapProtocolOutput{
+ KeyUnwrapResults: KeyUnwrapResults{OptsData: unwrappedKey},
+ })
+ }
+ return nil, errors.New("unknown protocol")
+}
+
+func TestKeyWrapKeyProviderCommandSuccess(t *testing.T) {
+ testConfigFile := "config.json"
+ os.Setenv("OCICRYPT_KEYPROVIDER_CONFIG", testConfigFile)
+ //Config File with executable for key wrap
+ configFile1 := `{"key-providers": {
+ "keyprovider-1": {
+ "cmd": {
+ "path": "/usr/lib/keyprovider-1-wrapkey",
+ "args": []
+ }
+ }
+ }}
+ `
+ //Config File with executable for key unwrap
+ configFile2 := `{"key-providers": {
+ "keyprovider-1": {
+ "cmd": {
+ "path": "/usr/lib/keyprovider-1-unwrapkey",
+ "args": []
+ }
+ }
+ }}
+ `
+ configFile, _ := os.OpenFile(testConfigFile, os.O_CREATE|os.O_WRONLY, 0644)
+ _, err := configFile.Write([]byte(configFile1))
+ assert.NoError(t, err)
+ configFile.Close()
+
+ optsData := []byte("data to be encrypted")
+
+ ic, _ := keyprovider_config.GetConfiguration()
+ keyWrapper := NewKeyWrapper("keyprovider-1", ic.KeyProviderConfig["keyprovider-1"])
+
+ parameters := make(map[string][][]byte)
+ parameters["keyprovider-1"] = nil
+ ec := config.EncryptConfig{
+ Parameters: parameters,
+ DecryptConfig: config.DecryptConfig{},
+ }
+ encryptingKey = []byte("passphrasewhichneedstobe32bytes!")
+ decryptingKey = []byte("passphrasewhichneedstobe32bytes!")
+ runner = TestRunner{}
+ keyWrapOutput, err := keyWrapper.WrapKeys(&ec, optsData)
+ assert.NoError(t, err)
+
+ configFile, _ = os.OpenFile(testConfigFile, os.O_CREATE|os.O_WRONLY, 0644)
+ _, err = configFile.Write([]byte(configFile2))
+ assert.NoError(t, err)
+ configFile.Close()
+
+ ic, _ = keyprovider_config.GetConfiguration()
+ keyWrapper = NewKeyWrapper("keyprovider-1", ic.KeyProviderConfig["keyprovider-1"])
+ dp := make(map[string][][]byte)
+ dp["keyprovider-1"] = append(dp["keyprovider-1"], []byte("Supported Protocol"))
+
+ dc := config.DecryptConfig{
+ Parameters: dp,
+ }
+ keyUnWrapOutput, err := keyWrapper.UnwrapKey(&dc, keyWrapOutput)
+ assert.NoError(t, err)
+ assert.Equal(t, optsData, keyUnWrapOutput)
+ os.Remove(testConfigFile)
+}
+
+func TestKeyWrapKeyProviderCommandFail(t *testing.T) {
+ testConfigFile := "config.json"
+ os.Setenv("OCICRYPT_KEYPROVIDER_CONFIG", testConfigFile)
+ //Config File with executable for key wrap
+ configFile1 := `{"key-providers": {
+ "keyprovider-1": {
+ "cmd": {
+ "path": "/usr/lib/keyprovider-1-wrapkey",
+ "args": []
+ }
+ },
+ "keyprovider-2": {
+ "cmd": {
+ "path": "/usr/lib/keyprovider-2-wrapkey",
+ "args": []
+ }
+ }
+ }}
+ `
+ //Config File with executable for key unwrap
+ configFile2 := `{"key-providers": {
+ "keyprovider-1": {
+ "cmd": {
+ "path": "/usr/lib/keyprovider-1-unwrapkey",
+ "args": []
+ }
+ },
+ "keyprovider-2": {
+ "cmd": {
+ "path": "/usr/lib/keyprovider-2-unwrapkey",
+ "args": []
+ }
+ }
+ }}
+ `
+ configFile, _ := os.OpenFile(testConfigFile, os.O_CREATE|os.O_WRONLY, 0644)
+ _, err := configFile.Write([]byte(configFile1))
+ assert.NoError(t, err)
+ configFile.Close()
+
+ optsData := []byte("data to be encrypted")
+ ic, _ := keyprovider_config.GetConfiguration()
+ keyWrapper := NewKeyWrapper("keyprovider-1", ic.KeyProviderConfig["keyprovider-1"])
+
+ parameters := make(map[string][][]byte)
+ parameters["keyprovider-1"] = nil
+ ec := config.EncryptConfig{
+ Parameters: parameters,
+ DecryptConfig: config.DecryptConfig{},
+ }
+ encryptingKey = []byte("passphrasewhichneedstobe32bytes!")
+ decryptingKey = []byte("wrongphrasewhichneedstobe32bytes")
+ runner = TestRunner{}
+ keyWrapOutput, err := keyWrapper.WrapKeys(&ec, optsData)
+ assert.NoError(t, err)
+
+ configFile, _ = os.OpenFile(testConfigFile, os.O_CREATE|os.O_WRONLY, 0644)
+ _, err = configFile.Write([]byte(configFile2))
+ assert.NoError(t, err)
+ configFile.Close()
+
+ dp := make(map[string][][]byte)
+ dp["keyprovider-1"] = append(dp["keyprovider-1"], []byte("Supported Protocol"))
+
+ dc := config.DecryptConfig{
+ Parameters: dp,
+ }
+ keyUnWrapOutput, _ := keyWrapper.UnwrapKey(&dc, keyWrapOutput)
+ assert.Nil(t, keyUnWrapOutput)
+ os.Remove(testConfigFile)
+}
+
+func TestKeyWrapKeyProviderGRPCSuccess(t *testing.T) {
+ path := "config.json"
+ os.Setenv("OCICRYPT_KEYPROVIDER_CONFIG", path)
+ filecontent := `{"key-providers": {
+ "keyprovider-1": {
+ "grpc": "localhost:50051"
+ },
+ "keyprovider-2": {
+ "grpc": "localhost:3990"
+ },
+ "keyprovider-3": {
+ "cmd": {
+ "path": "/usr/lib/keyprovider-2-unwrapkey",
+ "args": []
+ }
+ }
+
+ }}
+ `
+ tempFile, _ := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
+ _, err := tempFile.Write([]byte(filecontent))
+ assert.NoError(t, err)
+ tempFile.Close()
+
+ optsData := []byte("data to be encrypted")
+
+ ic, _ := keyprovider_config.GetConfiguration()
+ keyWrapper := NewKeyWrapper("keyprovider-1", ic.KeyProviderConfig["keyprovider-1"])
+
+ parameters := make(map[string][][]byte)
+ parameters["keyprovider-1"] = nil
+ ec := config.EncryptConfig{
+ Parameters: parameters,
+ DecryptConfig: config.DecryptConfig{},
+ }
+
+ runner = TestRunner{}
+ encryptingKey = []byte("passphrasewhichneedstobe32bytes!")
+ decryptingKey = encryptingKey
+ keyWrapOutput, err := keyWrapper.WrapKeys(&ec, optsData)
+ assert.NoError(t, err)
+
+ dp := make(map[string][][]byte)
+ dp["keyprovider-1"] = append(dp["keyprovider-1"], []byte("Supported Protocol"))
+
+ dc := config.DecryptConfig{
+ Parameters: dp,
+ }
+ keyUnWrapOutput, err := keyWrapper.UnwrapKey(&dc, keyWrapOutput)
+ assert.NoError(t, err)
+ assert.Equal(t, optsData, keyUnWrapOutput)
+ os.Remove(path)
+}
diff --git a/keywrap/keywrap.go b/keywrap/keywrap.go
new file mode 100644
index 0000000..ed25e7d
--- /dev/null
+++ b/keywrap/keywrap.go
@@ -0,0 +1,48 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package keywrap
+
+import (
+ "github.com/containers/ocicrypt/config"
+)
+
+// KeyWrapper is the interface used for wrapping keys using
+// a specific encryption technology (pgp, jwe)
+type KeyWrapper interface {
+ WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error)
+ UnwrapKey(dc *config.DecryptConfig, annotation []byte) ([]byte, error)
+ GetAnnotationID() string
+
+ // NoPossibleKeys returns true if there is no possibility of performing
+ // decryption for parameters provided.
+ NoPossibleKeys(dcparameters map[string][][]byte) bool
+
+ // GetPrivateKeys (optional) gets the array of private keys. It is an optional implementation
+ // as in some key services, a private key may not be exportable (i.e. HSM)
+ // If not implemented, return nil
+ GetPrivateKeys(dcparameters map[string][][]byte) [][]byte
+
+ // GetKeyIdsFromPacket (optional) gets a list of key IDs. This is optional as some encryption
+ // schemes may not have a notion of key IDs
+ // If not implemented, return the nil slice
+ GetKeyIdsFromPacket(packet string) ([]uint64, error)
+
+ // GetRecipients (optional) gets a list of recipients. It is optional due to the validity of
+ // recipients in a particular encryptiong scheme
+ // If not implemented, return the nil slice
+ GetRecipients(packet string) ([]string, error)
+}
diff --git a/keywrap/pgp/keywrapper_gpg.go b/keywrap/pgp/keywrapper_gpg.go
new file mode 100644
index 0000000..4ab9bd9
--- /dev/null
+++ b/keywrap/pgp/keywrapper_gpg.go
@@ -0,0 +1,272 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package pgp
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "net/mail"
+ "strconv"
+ "strings"
+
+ "github.com/containers/ocicrypt/config"
+ "github.com/containers/ocicrypt/keywrap"
+ "golang.org/x/crypto/openpgp"
+ "golang.org/x/crypto/openpgp/packet"
+)
+
+type gpgKeyWrapper struct {
+}
+
+// NewKeyWrapper returns a new key wrapping interface for pgp
+func NewKeyWrapper() keywrap.KeyWrapper {
+ return &gpgKeyWrapper{}
+}
+
+var (
+ // GPGDefaultEncryptConfig is the default configuration for layer encryption/decryption
+ GPGDefaultEncryptConfig = &packet.Config{
+ Rand: rand.Reader,
+ DefaultHash: crypto.SHA256,
+ DefaultCipher: packet.CipherAES256,
+ CompressionConfig: &packet.CompressionConfig{Level: 0}, // No compression
+ RSABits: 2048,
+ }
+)
+
+func (kw *gpgKeyWrapper) GetAnnotationID() string {
+ return "org.opencontainers.image.enc.keys.pgp"
+}
+
+// WrapKeys wraps the session key for recpients and encrypts the optsData, which
+// describe the symmetric key used for encrypting the layer
+func (kw *gpgKeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) {
+ ciphertext := new(bytes.Buffer)
+ el, err := kw.createEntityList(ec)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create entity list: %w", err)
+ }
+ if len(el) == 0 {
+ // nothing to do -- not an error
+ return nil, nil
+ }
+
+ plaintextWriter, err := openpgp.Encrypt(ciphertext,
+ el, /*EntityList*/
+ nil, /* Sign*/
+ nil, /* FileHint */
+ GPGDefaultEncryptConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ if _, err = plaintextWriter.Write(optsData); err != nil {
+ return nil, err
+ } else if err = plaintextWriter.Close(); err != nil {
+ return nil, err
+ }
+ return ciphertext.Bytes(), err
+}
+
+// UnwrapKey unwraps the symmetric key with which the layer is encrypted
+// This symmetric key is encrypted in the PGP payload.
+func (kw *gpgKeyWrapper) UnwrapKey(dc *config.DecryptConfig, pgpPacket []byte) ([]byte, error) {
+ pgpPrivateKeys, pgpPrivateKeysPwd, err := kw.getKeyParameters(dc.Parameters)
+ if err != nil {
+ return nil, err
+ }
+
+ for idx, pgpPrivateKey := range pgpPrivateKeys {
+ r := bytes.NewBuffer(pgpPrivateKey)
+ entityList, err := openpgp.ReadKeyRing(r)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse private keys: %w", err)
+ }
+
+ var prompt openpgp.PromptFunction
+ if len(pgpPrivateKeysPwd) > idx {
+ responded := false
+ prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
+ if responded {
+ return nil, fmt.Errorf("don't seem to have the right password")
+ }
+ responded = true
+ for _, key := range keys {
+ if key.PrivateKey != nil {
+ _ = key.PrivateKey.Decrypt(pgpPrivateKeysPwd[idx])
+ }
+ }
+ return pgpPrivateKeysPwd[idx], nil
+ }
+ }
+
+ r = bytes.NewBuffer(pgpPacket)
+ md, err := openpgp.ReadMessage(r, entityList, prompt, GPGDefaultEncryptConfig)
+ if err != nil {
+ continue
+ }
+ // we get the plain key options back
+ optsData, err := io.ReadAll(md.UnverifiedBody)
+ if err != nil {
+ continue
+ }
+ return optsData, nil
+ }
+ return nil, errors.New("PGP: No suitable key found to unwrap key")
+}
+
+// GetKeyIdsFromWrappedKeys converts the base64 encoded PGPPacket to uint64 keyIds
+func (kw *gpgKeyWrapper) GetKeyIdsFromPacket(b64pgpPackets string) ([]uint64, error) {
+
+ var keyids []uint64
+ for _, b64pgpPacket := range strings.Split(b64pgpPackets, ",") {
+ pgpPacket, err := base64.StdEncoding.DecodeString(b64pgpPacket)
+ if err != nil {
+ return nil, fmt.Errorf("could not decode base64 encoded PGP packet: %w", err)
+ }
+ newids, err := kw.getKeyIDs(pgpPacket)
+ if err != nil {
+ return nil, err
+ }
+ keyids = append(keyids, newids...)
+ }
+ return keyids, nil
+}
+
+// getKeyIDs parses a PGPPacket and gets the list of recipients' key IDs
+func (kw *gpgKeyWrapper) getKeyIDs(pgpPacket []byte) ([]uint64, error) {
+ var keyids []uint64
+
+ kbuf := bytes.NewBuffer(pgpPacket)
+ packets := packet.NewReader(kbuf)
+ParsePackets:
+ for {
+ p, err := packets.Next()
+ if err == io.EOF {
+ break ParsePackets
+ }
+ if err != nil {
+ return []uint64{}, fmt.Errorf("packets.Next() failed: %w", err)
+ }
+ switch p := p.(type) {
+ case *packet.EncryptedKey:
+ keyids = append(keyids, p.KeyId)
+ case *packet.SymmetricallyEncrypted:
+ break ParsePackets
+ }
+ }
+ return keyids, nil
+}
+
+// GetRecipients converts the wrappedKeys to an array of recipients
+func (kw *gpgKeyWrapper) GetRecipients(b64pgpPackets string) ([]string, error) {
+ keyIds, err := kw.GetKeyIdsFromPacket(b64pgpPackets)
+ if err != nil {
+ return nil, err
+ }
+ var array []string
+ for _, keyid := range keyIds {
+ array = append(array, "0x"+strconv.FormatUint(keyid, 16))
+ }
+ return array, nil
+}
+
+func (kw *gpgKeyWrapper) NoPossibleKeys(dcparameters map[string][][]byte) bool {
+ return len(kw.GetPrivateKeys(dcparameters)) == 0
+}
+
+func (kw *gpgKeyWrapper) GetPrivateKeys(dcparameters map[string][][]byte) [][]byte {
+ return dcparameters["gpg-privatekeys"]
+}
+
+func (kw *gpgKeyWrapper) getKeyParameters(dcparameters map[string][][]byte) ([][]byte, [][]byte, error) {
+
+ privKeys := kw.GetPrivateKeys(dcparameters)
+ if len(privKeys) == 0 {
+ return nil, nil, errors.New("GPG: Missing private key parameter")
+ }
+
+ return privKeys, dcparameters["gpg-privatekeys-passwords"], nil
+}
+
+// createEntityList creates the opengpg EntityList by reading the KeyRing
+// first and then filtering out recipients' keys
+func (kw *gpgKeyWrapper) createEntityList(ec *config.EncryptConfig) (openpgp.EntityList, error) {
+ pgpPubringFile := ec.Parameters["gpg-pubkeyringfile"]
+ if len(pgpPubringFile) == 0 {
+ return nil, nil
+ }
+ r := bytes.NewReader(pgpPubringFile[0])
+
+ entityList, err := openpgp.ReadKeyRing(r)
+ if err != nil {
+ return nil, err
+ }
+
+ gpgRecipients := ec.Parameters["gpg-recipients"]
+ if len(gpgRecipients) == 0 {
+ return nil, nil
+ }
+
+ rSet := make(map[string]int)
+ for _, r := range gpgRecipients {
+ rSet[string(r)] = 0
+ }
+
+ var filteredList openpgp.EntityList
+ for _, entity := range entityList {
+ for k := range entity.Identities {
+ addr, err := mail.ParseAddress(k)
+ if err != nil {
+ return nil, err
+ }
+ for _, r := range gpgRecipients {
+ recp := string(r)
+ if strings.Compare(addr.Name, recp) == 0 || strings.Compare(addr.Address, recp) == 0 {
+ filteredList = append(filteredList, entity)
+ rSet[recp] = rSet[recp] + 1
+ }
+ }
+ }
+ }
+
+ // make sure we found keys for all the Recipients...
+ var buffer bytes.Buffer
+ notFound := false
+ buffer.WriteString("PGP: No key found for the following recipients: ")
+
+ for k, v := range rSet {
+ if v == 0 {
+ if notFound {
+ buffer.WriteString(", ")
+ }
+ buffer.WriteString(k)
+ notFound = true
+ }
+ }
+
+ if notFound {
+ return nil, errors.New(buffer.String())
+ }
+
+ return filteredList, nil
+}
diff --git a/keywrap/pgp/keywrapper_gpg_test.go b/keywrap/pgp/keywrapper_gpg_test.go
new file mode 100644
index 0000000..4cce35f
--- /dev/null
+++ b/keywrap/pgp/keywrapper_gpg_test.go
@@ -0,0 +1,190 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package pgp
+
+import (
+ "testing"
+
+ "github.com/containers/ocicrypt/config"
+)
+
+var validGpgCcs = []*config.CryptoConfig{
+ // Key 1
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-pubkeyringfile": {gpgPubKeyRing},
+ "gpg-recipients": {gpgRecipient1},
+ },
+ DecryptConfig: config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-privatekeys": {gpgPrivKey1},
+ },
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-privatekeys": {gpgPrivKey1},
+ },
+ },
+ },
+
+ // Key 2
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-pubkeyringfile": {gpgPubKeyRing},
+ "gpg-recipients": {gpgRecipient2},
+ },
+ DecryptConfig: config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-privatekeys": {gpgPrivKey2},
+ },
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-privatekeys": {gpgPrivKey2},
+ },
+ },
+ },
+
+ // Key 1 without enc private key
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-pubkeyringfile": {gpgPubKeyRing},
+ "gpg-recipients": {gpgRecipient1},
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-privatekeys": {gpgPrivKey1},
+ },
+ },
+ },
+
+ // Key 2 without enc private key
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-pubkeyringfile": {gpgPubKeyRing},
+ "gpg-recipients": {gpgRecipient2},
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-privatekeys": {gpgPrivKey2},
+ },
+ },
+ },
+}
+
+var invalidGpgCcs = []*config.CryptoConfig{
+ // Client key 1 public with client 2 private decrypt
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-pubkeyringfile": {gpgPubKeyRing},
+ "gpg-recipients": {gpgRecipient1},
+ },
+ },
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-privatekeys": {gpgPrivKey2},
+ },
+ },
+ },
+
+ // Client key 1 public with no private key
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-pubkeyringfile": {gpgPubKeyRing},
+ "gpg-recipients": {gpgRecipient1},
+ },
+ },
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{},
+ },
+ },
+
+ // Invalid Client key 1 private key
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-pubkeyringfile": {gpgPrivKey1},
+ "gpg-recipients": {gpgRecipient1},
+ },
+ },
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "gpg-privatekeys": {gpgPrivKey1},
+ },
+ },
+ },
+}
+
+func TestKeyWrapGpgSuccess(t *testing.T) {
+ for _, cc := range validGpgCcs {
+ kw := NewKeyWrapper()
+
+ data := []byte("This is some secret text")
+
+ wk, err := kw.WrapKeys(cc.EncryptConfig, data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(data) != string(ud) {
+ t.Fatal("Strings don't match")
+ }
+ }
+}
+
+func TestKeyWrapGpgInvalid(t *testing.T) {
+ for _, cc := range invalidGpgCcs {
+ kw := NewKeyWrapper()
+
+ data := []byte("This is some secret text")
+
+ wk, err := kw.WrapKeys(cc.EncryptConfig, data)
+ if err != nil {
+ return
+ }
+
+ ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
+ if err != nil {
+ return
+ }
+
+ if string(data) != string(ud) {
+ return
+ }
+
+ t.Fatal("Successfully wrap for invalid crypto config")
+ }
+}
diff --git a/keywrap/pgp/testingkeys_test.go b/keywrap/pgp/testingkeys_test.go
new file mode 100644
index 0000000..0914275
--- /dev/null
+++ b/keywrap/pgp/testingkeys_test.go
@@ -0,0 +1,178 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package pgp
+
+import (
+ "encoding/base64"
+ "strings"
+)
+
+func b64Dec(str string) string {
+ str = strings.Replace(str, " ", "", -1)
+ s, err := base64.StdEncoding.DecodeString(str)
+ if err != nil {
+ panic(err)
+ }
+
+ return string(s)
+}
+
+var (
+ // gpg2 --export
+ gpgPubKeyRing = []byte(b64Dec(`mQENBF9qNH4BCADnPy49qS3b36Sf0CjBL98lvNqOMotHupF0JUvNYcQq39OmOcRVUu1DVtWw7YDc
+VToO2gM+xSEQ677xxu+k0VcpfyGYQRoQSTxkvXlH9Qb9nZouizy0DstWwgquePRiK7sLKPbiZOcI
+XcYBKUwR6oQM2aYTuzaXax5wyqejczwOPqZ7Ww5aA9r2a1xEepSEjxPJ7+zNw3k2nWmL2uvX/gx7
+yCn78N3jQhLx8AMIE7eLk0QMTi1LldFWGz2V3z1SBOkdn2eUTsrQs2tBrq1oMEVHYwZqM6n+PW2S
+qhycrj6sVoK2vyfrC4E/bz7Spn4qIF3Q/ZShpHEI5lSELAYTcJtZABEBAAG0G3Rlc3RrZXkxIDx0
+ZXN0a2V5MUBrZXkub3JnPokBVAQTAQgAPgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgBYhBEH3
+fP8yEk2a+7XT5wexL41UPN6LBQJfajSdBQkSzAMfAAoJEAexL41UPN6LEvAIANRR49Pq66v674qg
+6J6v5o5Q9VLSkAJSqRiajkfQks2L164LQglW98ijvTA1s5X4YA+zspllwm29uOwl9PGZmmxC5Oj4
+7W+djHmluM5IToEezCC6sMr/ay0C5zbb2+H4pgZvqDv6/GZKCmXzdJnVag8T8kT0gmL7DnpivbWH
+VTjr4dVFWedLzmmG3lWVJpmXDsJ/UOunA9jnmvVCwkvAa0JG7KOwtG/pNOyki7MHPk6NbDaT9XL1
+5kX9e7ZcTHHy8Z3HG0e5y6HC6puOicKxwS4ywbm82OXKcgcTCy6IRC9baroEKRwAs5hero3ziTJ1
+eO/zye38i5yKaWyuh37IDVy5AQ0EX2o0fgEIAO3fcJRGYKVNCziQAAkqYDdU6zkt96aFI+LDr3Rw
+IYcdfoP4IXW0IqSQIv2YdzWLUbJNTszSxzbwVdpOLTRYctnKu/AUkzJIDppXXVGEGmeQgu21VjwZ
+qEvcxsrAOpZQ3Sg1yzy2q4U4T3kKpB7BQ7ZqOzBdFfQCC+PDpXhcxx2ubYbUcAov5bJLQwu0jteW
+HizIVin6mnpW7lv7pSkm0YczsZ41ODlGj+fWiEUI542zZRzgHDSu8kCVJWQtM/rSVEnZgFEAuZQx
+e9F936ZlyzqPYibyIiU7PDy3vR4i3S2kWp2CExXvSaGqIRcWj5qb5PJDnozfh0KuqnSrJxi+5GUA
+EQEAAYkBPAQYAQgAJgIbDBYhBEH3fP8yEk2a+7XT5wexL41UPN6LBQJfajSsBQkSzAMuAAoJEAex
+L41UPN6L/PAIAIsfpMF1c34C2S2FcdTjEXtatj3CQCqt+n6PKWxh9+siLQE8cTpvGl3chRsKtVF6
+BZqX8oqMkK9pEKlVfXgxcgtYjF908YHEWyor3D/5WE0xVRFlXfqQZGwoeDgsjCq6tQ7evbGmWwc4
+6eeCbAk0Um3idHF7IyNJ7ubN8r3rFL66r5+uk8rOPBBwXL6Qiez7oUXbXEz3MA0pgtnjp3UoGSLz
+MP8zZYsbq6IgodADjTiVD/1iUo1qT53PbXwIxMDkJhb2v+qz7tQPiU+6nbx6WtP2iaiPcZZF4Lln
+fV9On5hkutLSfk9Fum+4c05XvzI5BR+gKfjDno6HFDVQwxyLFguZAQ0EX2o1GQEIAMVoSxGGnjdG
+6usklWZArEOEMnE+wtkfThJaMs+8At3jvIEAmMpJQ0nLCtLU/HVtvkWd55KcGuJSGYqOJWEvaEp9
+aMd1+zucIrgyK43BS1n+2UqeJVOsEGnsOpN8jEIdUBDed/Q0AtlT3TngxxIwAscbcV8RpvPdRyWi
+h2AFhbKoUQnDjxXj9YgcCSByV8BFblg+2x458rIqB/lVDUzhfcR/CMPmNEdfFKvbjU9dizspAURh
+u41vMQdz0/vKDaFRHHkOXfEwKM+XKd0jz8CxOqjQxPGQNhxORo1aov0GxTKMoSjHsKDrbmElVaLP
+4IZc1euLple8ROJ80UgiLI2EiZ0AEQEAAbQbdGVzdGtleTIgPHRlc3RrZXkyQGtleS5vcmc+iQFU
+BBMBCAA+AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEEgA3KabisMtZshKKeeeCqSknvgDEF
+Al9qNSwFCRLMAxMACgkQeeCqSknvgDErgQgAqL4iQiaQ68kU2+fFSheeaqACbiO/ZWEIysZPAmpv
+Em5RgFqTKcSKNSaolPk6tfoFWZUoHOKPk03NE42ksu+hwLFQPXGRpLt8Sby+LeW+5uhqsGORjFC5
+FIapGlzoiPjujzyhJBOYUeMWymRLtTAV22lU3La83sXrk1O2p3/mVG29eMdInf6WhZYDKgvRUa8I
+EC+SbONxP60+p4P4EX+AGl6yUyse7WaUw437qPnWdkLmquNI4NiPN9yJkdmVo9A1aMtSE1D6TyzZ
+wUaDDMzrPiCbgojmGb8G6X3UD8wNEodTC6pbcAD15hxB7sbYvg6Yv5SJjXNYJaCUyNlk/cFmrbkB
+DQRfajUZAQgA2IciErApMP48Bk36h+1l35fCJ8eVZjhczEom6Hos2pDMJ5hjAIat8sNVb/FxoNox
+2hpIIZY/Cy9YDPJ8JDul896biX8otJ6NyGDFiIfJ4Jn8EZjFd7nTFXqFpy8jowh906Lf8fTc1b2J
+UguoJ6ygHZhbSpMiBBFieo0nGtAsyfoTHz/Fme81YCmTUkmZ6V2m05QG+eMrRgLYgmVDOiaIhxUG
+FiP1cPcTFwxLwN/2ZaYWBUcA74sPj+vvduFUyitT6Vu2t+11tFDlKD3/jcA9fHONw1ontXM4Gq3r
+bhkmiYQ6eqz6nB60zPKwW49q4BTjR0cjc3PcgoXQHhbJi5OCXQARAQABiQE8BBgBCAAmAhsMFiEE
+gA3KabisMtZshKKeeeCqSknvgDEFAl9qNToFCRLMAyEACgkQeeCqSknvgDGb4Qf+MUHc5XwvGMeB
+NU5VHs6fFUn60g40VLx9yX7RJwOwPGt3Jlkxg8VEXRZ+2AW3HgWkTHn1bPUheqm6ImydYZ+rV3PB
+kIpkPH1rC6KKefif/WJ8f/NfMR6JIYbldZ18TGyO6ZQIz17qA0jDQ8gXG9WkceGqcBpStVZpj7xS
+NYf8zDQNn8nT1S5su/uvUszOyO/PAjVWo4Vr4/D1YsSJ3SgMqmQsBvDKGmsHJbTxWMjemGTbEtrF
+16ohCrtvym1QLgkTTRUTNMMKPsxkvLaOqfjPgQ624cTr7FiUKmhcIVj18SLLuUmsjQjiV0Z4+glx
+ufVaRIk55HQJvh0rjekUUSLmCQ==`))
+
+ gpgRecipient1 = []byte("testkey1@key.org")
+
+ gpgRecipient2 = []byte("testkey2@key.org")
+
+ // gpg2 --export-secret-key testkey1
+ gpgPrivKey1 = []byte(b64Dec(`lQOYBF9qNH4BCADnPy49qS3b36Sf0CjBL98lvNqOMotHupF0JUvNYcQq39OmOcRVUu1DVtWw7YDc
+VToO2gM+xSEQ677xxu+k0VcpfyGYQRoQSTxkvXlH9Qb9nZouizy0DstWwgquePRiK7sLKPbiZOcI
+XcYBKUwR6oQM2aYTuzaXax5wyqejczwOPqZ7Ww5aA9r2a1xEepSEjxPJ7+zNw3k2nWmL2uvX/gx7
+yCn78N3jQhLx8AMIE7eLk0QMTi1LldFWGz2V3z1SBOkdn2eUTsrQs2tBrq1oMEVHYwZqM6n+PW2S
+qhycrj6sVoK2vyfrC4E/bz7Spn4qIF3Q/ZShpHEI5lSELAYTcJtZABEBAAEAB/wMWcFCPVIr82Cj
+QXafxMsGBLVmkVgDg3knyyMmi8FyqcQv1VeBWB3AcjeVDMZMXkfsyaORO22V7gVje+TKOH0PhBD7
+BQUbmBG/7qe22mUecAevUzPxiPW+wzvXUDH7OUsy4CP5ePqm5X1BDB/aOByH5Cr81Euo4Cl+zDAS
+aIHtX7y33WwB8/ybJpcp14tF75Wb0CEzFGeNX+VqwWmppexuvvRkzPiTNOAk2k9domb3JHbrfifs
+0HVkijUEW3Ke7yuOmci0wnhoHfJOzMPWtJYYEj//xsoQl5TN3rl5oLj5WZN4uIoYKBj8nZmbfkWd
+N1WF4xYSANisd6R0z2CWB2chBADonAYjJ2uIC516DOClxMlnH547olOw1YUNL8VGC7Wau0OYXnnZ
+K/YiOYAxQlfP0ZNu8je5K7QkI8qoy9HDSRKxzO/2w0kA0M1B8ZvsgFMRgx+fYjrlETSbuwuel5x9
+3i7M3o7BYUipNImSzGG+i18AQAP/n0Bgb/IQ2bzt9nxVEQQA/oAT9qvNgkg0dFnjPqKZxTXihZ6C
+1/31+6khFNWxch+kPtdYT0j9jDZwIe9wfjQ3qx/AFYXvcqZlKlCAHx1+rXL/nRkfmC+955SibBrU
+BRM1X4hYAYwievVURNZD3P6a6kfekrLsbjJil9A6ibf0fU9mKHLBMVJod6IufBSwwckD/09P3ler
+DMi7LyFK1AAWYuNPATfbKjU+Dg57r1xCpbPNiU2OXeI0m4bCaPBh8Ga1rg21/CXNENs2SyU4Gsp5
+TBkzq0bYqXuf2OWOBh3W5LDt/EKGvzn1q3i+Y6+JEBhZKhgHxbmMIU0IjoHs09TMDQXk7Ro1GuYs
+emydcKko00sNP0S0G3Rlc3RrZXkxIDx0ZXN0a2V5MUBrZXkub3JnPokBVAQTAQgAPgIbAwULCQgH
+AgYVCgkICwIEFgIDAQIeAQIXgBYhBEH3fP8yEk2a+7XT5wexL41UPN6LBQJfajSdBQkSzAMfAAoJ
+EAexL41UPN6LEvAIANRR49Pq66v674qg6J6v5o5Q9VLSkAJSqRiajkfQks2L164LQglW98ijvTA1
+s5X4YA+zspllwm29uOwl9PGZmmxC5Oj47W+djHmluM5IToEezCC6sMr/ay0C5zbb2+H4pgZvqDv6
+/GZKCmXzdJnVag8T8kT0gmL7DnpivbWHVTjr4dVFWedLzmmG3lWVJpmXDsJ/UOunA9jnmvVCwkvA
+a0JG7KOwtG/pNOyki7MHPk6NbDaT9XL15kX9e7ZcTHHy8Z3HG0e5y6HC6puOicKxwS4ywbm82OXK
+cgcTCy6IRC9baroEKRwAs5hero3ziTJ1eO/zye38i5yKaWyuh37IDVydA5gEX2o0fgEIAO3fcJRG
+YKVNCziQAAkqYDdU6zkt96aFI+LDr3RwIYcdfoP4IXW0IqSQIv2YdzWLUbJNTszSxzbwVdpOLTRY
+ctnKu/AUkzJIDppXXVGEGmeQgu21VjwZqEvcxsrAOpZQ3Sg1yzy2q4U4T3kKpB7BQ7ZqOzBdFfQC
+C+PDpXhcxx2ubYbUcAov5bJLQwu0jteWHizIVin6mnpW7lv7pSkm0YczsZ41ODlGj+fWiEUI542z
+ZRzgHDSu8kCVJWQtM/rSVEnZgFEAuZQxe9F936ZlyzqPYibyIiU7PDy3vR4i3S2kWp2CExXvSaGq
+IRcWj5qb5PJDnozfh0KuqnSrJxi+5GUAEQEAAQAH/2J/D/3FuoUYBtpv/iPNcTPYLOJrX02LedWP
+E9rSB4AMPXPlze0QHvwnVuXNOSdpvfVnv4ZejPD5yYLwthUjvsLiCLobuuuqHKnaHSEA43IYy64k
+VUXjleV70LDpshjF+R2KUNKeDR3HuFi1iEnX2vLwv/uBv/Je2o+AVsclG6n04LUeXlJTjOdv31g7
+07He6aftp1OHLi0MVcomZXPbbqMOVzIPjpweGjI9HS32rg/z1C09c11zTKz2asdFdkl3fkfdsrwY
+dVp31EDy8JHbYmI3MflmIg6zgyOP5jtdjwWIMwXI9QK14Go+ZdHakA/d3QRcadQXwbWDkU16drWY
+EO0EAPUu6Ig+5grP1mAKyqxs+3zDTivk4IGnM76e0KSVnK3Ixh7JmpFdH9mIQQ5EF3pYsL8xspfN
+8gMlKqHEyll9YwbRRniUiaurP3WCgpTEfX+uS+1czk7trTIZKEVtM7wN9FS4R7WJEdvS3FnZ06B9
+G391duUR+QQmlWFjtCtladqDBAD4Xfe9kAvuO6m3j47EDW6SEKW6JlMgPzHWokVfeeGaeuvZfAZZ
+W0DGRnWRcgtD2BmF/qIoeyo7Df280lDi+LJLCKKgtEZc+7KdUIIOZWJV5BNXzc8AzULMcT7xs/lE
+Udp86Pkr8SefhZdfwAuk+PAEHJKha2oTQLoVRnl1L6Gw9wQAh/WNnBAIm/Zhn4BuABeaHt0/UXF6
+cKF25rJlOdF1kagyytYyEKK+cUjczdqdw7mxm7D4RmWZP+JnGH8w3hOssptDz5iV1K6pArSzg0dk
+Z45sQ9SNA1Ckxq0R54gSokoGpM4vGgY7QnKLOJE+R2W7Ko+XnH8NP8OjvviEyZbicJVHUIkBPAQY
+AQgAJgIbDBYhBEH3fP8yEk2a+7XT5wexL41UPN6LBQJfajSsBQkSzAMuAAoJEAexL41UPN6L/PAI
+AIsfpMF1c34C2S2FcdTjEXtatj3CQCqt+n6PKWxh9+siLQE8cTpvGl3chRsKtVF6BZqX8oqMkK9p
+EKlVfXgxcgtYjF908YHEWyor3D/5WE0xVRFlXfqQZGwoeDgsjCq6tQ7evbGmWwc46eeCbAk0Um3i
+dHF7IyNJ7ubN8r3rFL66r5+uk8rOPBBwXL6Qiez7oUXbXEz3MA0pgtnjp3UoGSLzMP8zZYsbq6Ig
+odADjTiVD/1iUo1qT53PbXwIxMDkJhb2v+qz7tQPiU+6nbx6WtP2iaiPcZZF4LlnfV9On5hkutLS
+fk9Fum+4c05XvzI5BR+gKfjDno6HFDVQwxyLFgs=`))
+
+ // gpg2 --export-secret-key testkey2
+ gpgPrivKey2 = []byte(b64Dec(`lQOYBF9qNRkBCADFaEsRhp43RurrJJVmQKxDhDJxPsLZH04SWjLPvALd47yBAJjKSUNJywrS1Px1
+bb5FneeSnBriUhmKjiVhL2hKfWjHdfs7nCK4MiuNwUtZ/tlKniVTrBBp7DqTfIxCHVAQ3nf0NALZ
+U9054McSMALHG3FfEabz3UcloodgBYWyqFEJw48V4/WIHAkgclfARW5YPtseOfKyKgf5VQ1M4X3E
+fwjD5jRHXxSr241PXYs7KQFEYbuNbzEHc9P7yg2hURx5Dl3xMCjPlyndI8/AsTqo0MTxkDYcTkaN
+WqL9BsUyjKEox7Cg625hJVWiz+CGXNXri6ZXvETifNFIIiyNhImdABEBAAEAB/9FzhvhfidbZ53x
+eXXE+zCPDWOi7O0Mxwed8LxP/e1LlljViyb8PQzovr48kGkXgy+JwY0eKEpPZnW2q44nQBLSaGdR
+RPSKfys91CvXjBb/o2EmBCcx38HMGucZuSyFwoTJ+kkTlwK84+1yJnxuf4Cz9I3R7tWJHWGnusHB
+ICLHaiKkLdFLzweD5IFz5ElTlPbGgFicWrkykllHWee/tOb7DUtj2u5NO7LZ9t8TJnD6hwRGgA89
+61d4U5j6FtW7pfSf7OeQ4s1X6JZE4q7Z/chu9cptoCgQ8SLjuRrgpiHQj4sXspjMwZOzNjFmeipB
+G/AvJsZ+gvQCG2XUX9hOR2VXBADKYFR8EXApKKLuZbD+khTKCvVi2GdGw3ceR7YvZc1tw7U/uSFb
+irwqvPQC79IzJogurpcJUBO4EpP0Vb6zgyPARmAO0Ky9+BEQ4qQYSv+k/0yseyb9GcMh3Nt0FMNt
+10XUJTTKemQqy1oFS3Zlm8rtJrD/3KPE7CDZL57WlegYxwQA+bbpRbboLpbfJM+JbvdAfssg+L4m
+bpv+Bqq7IOTHbKsOz5A942aLZQS4FXMyMkYbKR5hLRyMREMlBatFf3YP4n475M45FQNHv5spWfyf
+BvFPoX8xMS1CMuQ0xDuVBTedNhvf0n031tma5phzvEPc/AFzaC4j1V5gFERk0UjsTnsD/R0JUBWO
+MhNlO9ubno+MJJ8qi6catOVuaPWI4wdUx8+5b3iF/O5TuBue/+KOxRQfsYQUVYwvEPTmIjlcDnyP
+ZZUX/0S/goILb+0uQWFx10+Cgc8Glz2hhUq1Kwd4loerCK2UrkR7EEdO6ggvVKilgPI0GPQ/D72S
+MwPXM0kFjEDFPjy0G3Rlc3RrZXkyIDx0ZXN0a2V5MkBrZXkub3JnPokBVAQTAQgAPgIbAwULCQgH
+AgYVCgkICwIEFgIDAQIeAQIXgBYhBIANymm4rDLWbISinnngqkpJ74AxBQJfajUsBQkSzAMTAAoJ
+EHngqkpJ74AxK4EIAKi+IkImkOvJFNvnxUoXnmqgAm4jv2VhCMrGTwJqbxJuUYBakynEijUmqJT5
+OrX6BVmVKBzij5NNzRONpLLvocCxUD1xkaS7fEm8vi3lvuboarBjkYxQuRSGqRpc6Ij47o88oSQT
+mFHjFspkS7UwFdtpVNy2vN7F65NTtqd/5lRtvXjHSJ3+loWWAyoL0VGvCBAvkmzjcT+tPqeD+BF/
+gBpeslMrHu1mlMON+6j51nZC5qrjSODYjzfciZHZlaPQNWjLUhNQ+k8s2cFGgwzM6z4gm4KI5hm/
+Bul91A/MDRKHUwuqW3AA9eYcQe7G2L4OmL+UiY1zWCWglMjZZP3BZq2dA5gEX2o1GQEIANiHIhKw
+KTD+PAZN+oftZd+XwifHlWY4XMxKJuh6LNqQzCeYYwCGrfLDVW/xcaDaMdoaSCGWPwsvWAzyfCQ7
+pfPem4l/KLSejchgxYiHyeCZ/BGYxXe50xV6hacvI6MIfdOi3/H03NW9iVILqCesoB2YW0qTIgQR
+YnqNJxrQLMn6Ex8/xZnvNWApk1JJmeldptOUBvnjK0YC2IJlQzomiIcVBhYj9XD3ExcMS8Df9mWm
+FgVHAO+LD4/r73bhVMorU+lbtrftdbRQ5Sg9/43APXxzjcNaJ7VzOBqt624ZJomEOnqs+pwetMzy
+sFuPauAU40dHI3Nz3IKF0B4WyYuTgl0AEQEAAQAH/juTBplstZCgyoQTjWQ7uYVI3GcUfzMGO+YL
+WuQoxVGHeFxGjaq144NBIi8wF4rhrcir5X+0NnlN1+SMDQLtFG5iJ5ovjdQQMcNZeM/lSHKO+28e
+AOq9imnE8aP7kMsJCZGipQoNzHrUcMVNpsDvuogaBLgifj/vRpCgaIt0jnYtYejYKX+/LvaQu+KQ
+kGXa0VCyWQk/IT0ExOTCgfFaWp1BNzH3GwhnKtXp7gafcM2fBK8AExrBs9VeBWSRopRO2Koyq4hi
++9NSY1nY8pTixlYKzttIOLGjT+xhR/+gXmVzJGC4WueZWLWeLPER9pKap0rxRpFAM80z0UR8C1MN
+ksMEAOluyEwAY7OsnjyeGHcQc4YxM/u+AwMrcO/Wdm80k6ASb7GNiqc2FKMwhoXlEk+ee6i0F1MK
+6B7bLqOYQ4IsErIppEAAg96Td2ec8uWbSpGMcB17HJ6T2CKZHaEGSWVZAzCXVSt7fWHpXmqp2Eou
+MHgnlWUvJiex7txII2c4a/IzBADtdfj1EY2DOVoNnl8NDGQk+KvuwnxVyfFlAwczxVc5BGHqKDyq
+8FL7wfe2/HGN3Ff5mBPCNkKLv1qilf1KdyzXmVJ7S1d/99K+g9tXMmu+mHL13wIgiLp1l1qMVuWy
+RUY22JP9cKD6igm4HU3uIxeCWW8fNMSNQyO2ej0DrwjJLwQAo73wTAfPBxe7PxHS8HYslQ2Y2YJu
+ylGTY7n9pOCLlrfFXWVk0DW1pk/LlMXUcAp6i+BS9Y7wvv+VFvUmaz1yi56qwUW/Oeki2Mhiz7IA
+5VVkSPqg02N0upvb7efdK49YqC2/Ew/YExfaCWDc2fu6ZwX34mNHcFiw/HjRGwH3RbM+FokBPAQY
+AQgAJgIbDBYhBIANymm4rDLWbISinnngqkpJ74AxBQJfajU6BQkSzAMhAAoJEHngqkpJ74Axm+EH
+/jFB3OV8LxjHgTVOVR7OnxVJ+tIONFS8fcl+0ScDsDxrdyZZMYPFRF0WftgFtx4FpEx59Wz1IXqp
+uiJsnWGfq1dzwZCKZDx9awuiinn4n/1ifH/zXzEeiSGG5XWdfExsjumUCM9e6gNIw0PIFxvVpHHh
+qnAaUrVWaY+8UjWH/Mw0DZ/J09UubLv7r1LMzsjvzwI1VqOFa+Pw9WLEid0oDKpkLAbwyhprByW0
+8VjI3phk2xLaxdeqIQq7b8ptUC4JE00VEzTDCj7MZLy2jqn4z4EOtuHE6+xYlCpoXCFY9fEiy7lJ
+rI0I4ldGePoJcbn1WkSJOeR0Cb4dK43pFFEi5gk=`))
+)
diff --git a/keywrap/pkcs11/keywrapper_pkcs11.go b/keywrap/pkcs11/keywrapper_pkcs11.go
new file mode 100644
index 0000000..b9a83c5
--- /dev/null
+++ b/keywrap/pkcs11/keywrapper_pkcs11.go
@@ -0,0 +1,152 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package pkcs11
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/containers/ocicrypt/config"
+ "github.com/containers/ocicrypt/crypto/pkcs11"
+ "github.com/containers/ocicrypt/keywrap"
+ "github.com/containers/ocicrypt/utils"
+)
+
+type pkcs11KeyWrapper struct {
+}
+
+func (kw *pkcs11KeyWrapper) GetAnnotationID() string {
+ return "org.opencontainers.image.enc.keys.pkcs11"
+}
+
+// NewKeyWrapper returns a new key wrapping interface using pkcs11
+func NewKeyWrapper() keywrap.KeyWrapper {
+ return &pkcs11KeyWrapper{}
+}
+
+// WrapKeys wraps the session key for recpients and encrypts the optsData, which
+// describe the symmetric key used for encrypting the layer
+func (kw *pkcs11KeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) {
+ // append({}, ...) allocates a fresh backing array, and that's necessary to guarantee concurrent calls to WrapKeys (as in c/image/copy.Image)
+ // can't race writing to the same backing array.
+ pubKeys := append([][]byte{}, ec.Parameters["pkcs11-pubkeys"]...) // In Go 1.21, slices.Clone(ec.Parameters["pkcs11-pubkeys"])
+ pubKeys = append(pubKeys, ec.Parameters["pkcs11-yamls"]...)
+ pkcs11Recipients, err := addPubKeys(&ec.DecryptConfig, pubKeys)
+ if err != nil {
+ return nil, err
+ }
+ // no recipients is not an error...
+ if len(pkcs11Recipients) == 0 {
+ return nil, nil
+ }
+
+ jsonString, err := pkcs11.EncryptMultiple(pkcs11Recipients, optsData)
+ if err != nil {
+ return nil, fmt.Errorf("PKCS11 EncryptMulitple failed: %w", err)
+ }
+ return jsonString, nil
+}
+
+func (kw *pkcs11KeyWrapper) UnwrapKey(dc *config.DecryptConfig, jsonString []byte) ([]byte, error) {
+ var pkcs11PrivKeys []*pkcs11.Pkcs11KeyFileObject
+
+ privKeys := kw.GetPrivateKeys(dc.Parameters)
+ if len(privKeys) == 0 {
+ return nil, errors.New("No private keys found for PKCS11 decryption")
+ }
+
+ p11conf, err := p11confFromParameters(dc.Parameters)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, privKey := range privKeys {
+ key, err := utils.ParsePrivateKey(privKey, nil, "PKCS11")
+ if err != nil {
+ return nil, err
+ }
+ switch pkcs11PrivKey := key.(type) {
+ case *pkcs11.Pkcs11KeyFileObject:
+ if p11conf != nil {
+ pkcs11PrivKey.Uri.SetModuleDirectories(p11conf.ModuleDirectories)
+ pkcs11PrivKey.Uri.SetAllowedModulePaths(p11conf.AllowedModulePaths)
+ }
+ pkcs11PrivKeys = append(pkcs11PrivKeys, pkcs11PrivKey)
+ default:
+ continue
+ }
+ }
+
+ plaintext, err := pkcs11.Decrypt(pkcs11PrivKeys, jsonString)
+ if err == nil {
+ return plaintext, nil
+ }
+
+ return nil, fmt.Errorf("PKCS11: No suitable private key found for decryption: %w", err)
+}
+
+func (kw *pkcs11KeyWrapper) NoPossibleKeys(dcparameters map[string][][]byte) bool {
+ return len(kw.GetPrivateKeys(dcparameters)) == 0
+}
+
+func (kw *pkcs11KeyWrapper) GetPrivateKeys(dcparameters map[string][][]byte) [][]byte {
+ return dcparameters["pkcs11-yamls"]
+}
+
+func (kw *pkcs11KeyWrapper) GetKeyIdsFromPacket(_ string) ([]uint64, error) {
+ return nil, nil
+}
+
+func (kw *pkcs11KeyWrapper) GetRecipients(_ string) ([]string, error) {
+ return []string{"[pkcs11]"}, nil
+}
+
+func addPubKeys(dc *config.DecryptConfig, pubKeys [][]byte) ([]interface{}, error) {
+ var pkcs11Keys []interface{}
+
+ if len(pubKeys) == 0 {
+ return pkcs11Keys, nil
+ }
+
+ p11conf, err := p11confFromParameters(dc.Parameters)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, pubKey := range pubKeys {
+ key, err := utils.ParsePublicKey(pubKey, "PKCS11")
+ if err != nil {
+ return nil, err
+ }
+ switch pkcs11PubKey := key.(type) {
+ case *pkcs11.Pkcs11KeyFileObject:
+ if p11conf != nil {
+ pkcs11PubKey.Uri.SetModuleDirectories(p11conf.ModuleDirectories)
+ pkcs11PubKey.Uri.SetAllowedModulePaths(p11conf.AllowedModulePaths)
+ }
+ }
+ pkcs11Keys = append(pkcs11Keys, key)
+ }
+ return pkcs11Keys, nil
+}
+
+func p11confFromParameters(dcparameters map[string][][]byte) (*pkcs11.Pkcs11Config, error) {
+ if _, ok := dcparameters["pkcs11-config"]; ok {
+ return pkcs11.ParsePkcs11ConfigFile(dcparameters["pkcs11-config"][0])
+ }
+ return nil, nil
+}
diff --git a/keywrap/pkcs11/keywrapper_pkcs11_test.go b/keywrap/pkcs11/keywrapper_pkcs11_test.go
new file mode 100644
index 0000000..0536a8d
--- /dev/null
+++ b/keywrap/pkcs11/keywrapper_pkcs11_test.go
@@ -0,0 +1,217 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package pkcs11
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/containers/ocicrypt/config"
+ "github.com/containers/ocicrypt/crypto/pkcs11"
+ "github.com/containers/ocicrypt/utils"
+ "github.com/containers/ocicrypt/utils/softhsm"
+)
+
+var (
+ SOFTHSM_SETUP = "../../scripts/softhsm_setup"
+)
+
+func getPkcs11ConfigYaml(t *testing.T) []byte {
+ // we need to provide a configuration file so that on the various distros
+ // the libsofthsm2.so will be found by searching directories
+ mdyamls := pkcs11.GetDefaultModuleDirectoriesYaml("")
+ config := fmt.Sprintf("module-directories:\n"+
+ "%s"+
+ "allowed-module-paths:\n"+
+ "%s", mdyamls, mdyamls)
+ return []byte(config)
+}
+
+func createValidPkcs11Ccs(t *testing.T) ([]*config.CryptoConfig, *softhsm.SoftHSMSetup, error) {
+ shsm := softhsm.NewSoftHSMSetup()
+ pkcs11PubKeyUriStr, err := shsm.RunSoftHSMSetup(SOFTHSM_SETUP)
+ if err != nil {
+ return nil, shsm, err
+ }
+ pubKeyPem, err := shsm.RunSoftHSMGetPubkey(SOFTHSM_SETUP)
+ if err != nil {
+ return nil, shsm, err
+ }
+ pkcs11PrivKeyYaml := `
+pkcs11:
+ uri: ` + pkcs11PubKeyUriStr + `
+module:
+ env:
+ SOFTHSM2_CONF: ` + shsm.GetConfigFilename()
+
+ p11confYaml := getPkcs11ConfigYaml(t)
+
+ validPkcs11Ccs := []*config.CryptoConfig{
+ // Key 1
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "pkcs11-pubkeys": {[]byte(pubKeyPem)},
+ },
+ DecryptConfig: config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "pkcs11-yamls": {[]byte(pkcs11PrivKeyYaml)},
+ "pkcs11-config": {p11confYaml},
+ },
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "pkcs11-yamls": {[]byte(pkcs11PrivKeyYaml)},
+ "pkcs11-config": {p11confYaml},
+ },
+ },
+ },
+ // Key 2
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ // public and private key YAMLs are identical
+ "pkcs11-yamls": {[]byte(pkcs11PrivKeyYaml)},
+ },
+ DecryptConfig: config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "pkcs11-yamls": {[]byte(pkcs11PrivKeyYaml)},
+ "pkcs11-config": {p11confYaml},
+ },
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "pkcs11-yamls": {[]byte(pkcs11PrivKeyYaml)},
+ "pkcs11-config": {p11confYaml},
+ },
+ },
+ },
+ }
+ return validPkcs11Ccs, shsm, nil
+}
+
+func createInvalidPkcs11Ccs(t *testing.T) ([]*config.CryptoConfig, *softhsm.SoftHSMSetup, error) {
+ shsm := softhsm.NewSoftHSMSetup()
+ pkcs11PubKeyUriStr, err := shsm.RunSoftHSMSetup(SOFTHSM_SETUP)
+ if err != nil {
+ return nil, shsm, err
+ }
+ pubKey2Pem, _, err := utils.CreateRSATestKey(2048, nil, true)
+ if err != nil {
+ return nil, shsm, err
+ }
+ pkcs11PrivKeyYaml := `
+pkcs11:
+ uri: ` + pkcs11PubKeyUriStr + `
+module:
+ env:
+ SOFTHSM2_CONF: ` + shsm.GetConfigFilename()
+
+ p11confYaml := getPkcs11ConfigYaml(t)
+
+ invalidPkcs11Ccs := []*config.CryptoConfig{
+ // Key 1
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "pkcs11-pubkeys": {pubKey2Pem},
+ },
+ DecryptConfig: config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "pkcs11-yamls": {[]byte(pkcs11PrivKeyYaml)},
+ "pkcs11-config": {p11confYaml},
+ },
+ },
+ },
+
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "pkcs11-yamls": {[]byte(pkcs11PrivKeyYaml)},
+ "pkcs11-config": {p11confYaml},
+ },
+ },
+ },
+ }
+ return invalidPkcs11Ccs, shsm, nil
+}
+
+func TestKeyWrapPkcs11Success(t *testing.T) {
+ validPkcs11Ccs, shsm, err := createValidPkcs11Ccs(t)
+ defer shsm.RunSoftHSMTeardown(SOFTHSM_SETUP)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ os.Setenv("OCICRYPT_OAEP_HASHALG", "sha1")
+
+ for _, cc := range validPkcs11Ccs {
+ kw := NewKeyWrapper()
+
+ data := []byte("This is some secret text")
+
+ wk, err := kw.WrapKeys(cc.EncryptConfig, data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(data) != string(ud) {
+ t.Fatal("Strings don't match")
+ }
+ }
+}
+
+func TestKeyWrapPkcs11Invalid(t *testing.T) {
+ invalidPkcs11Ccs, shsm, err := createInvalidPkcs11Ccs(t)
+ defer shsm.RunSoftHSMTeardown(SOFTHSM_SETUP)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ os.Setenv("OCICRYPT_OAEP_HASHALG", "sha1")
+
+ for _, cc := range invalidPkcs11Ccs {
+ kw := NewKeyWrapper()
+
+ data := []byte("This is some secret text")
+
+ wk, err := kw.WrapKeys(cc.EncryptConfig, data)
+ if err != nil {
+ t.Fatalf("Wrapping should have worked")
+ }
+
+ ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
+ if err != nil {
+ continue
+ }
+
+ if string(data) != string(ud) {
+ t.Fatalf("Unwrapping should have failed already")
+ }
+
+ t.Fatal("Successfully wrapped and unwrapped with invalid crypto config")
+ }
+}
diff --git a/keywrap/pkcs7/keywrapper_pkcs7.go b/keywrap/pkcs7/keywrapper_pkcs7.go
new file mode 100644
index 0000000..603925d
--- /dev/null
+++ b/keywrap/pkcs7/keywrapper_pkcs7.go
@@ -0,0 +1,137 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package pkcs7
+
+import (
+ "crypto"
+ "crypto/x509"
+ "errors"
+ "fmt"
+
+ "github.com/containers/ocicrypt/config"
+ "github.com/containers/ocicrypt/keywrap"
+ "github.com/containers/ocicrypt/utils"
+ "go.mozilla.org/pkcs7"
+)
+
+type pkcs7KeyWrapper struct {
+}
+
+// NewKeyWrapper returns a new key wrapping interface using jwe
+func NewKeyWrapper() keywrap.KeyWrapper {
+ return &pkcs7KeyWrapper{}
+}
+
+func (kw *pkcs7KeyWrapper) GetAnnotationID() string {
+ return "org.opencontainers.image.enc.keys.pkcs7"
+}
+
+// WrapKeys wraps the session key for recpients and encrypts the optsData, which
+// describe the symmetric key used for encrypting the layer
+func (kw *pkcs7KeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) {
+ x509Certs, err := collectX509s(ec.Parameters["x509s"])
+ if err != nil {
+ return nil, err
+ }
+ // no recipients is not an error...
+ if len(x509Certs) == 0 {
+ return nil, nil
+ }
+
+ pkcs7.ContentEncryptionAlgorithm = pkcs7.EncryptionAlgorithmAES128GCM
+ return pkcs7.Encrypt(optsData, x509Certs)
+}
+
+func collectX509s(x509s [][]byte) ([]*x509.Certificate, error) {
+ if len(x509s) == 0 {
+ return nil, nil
+ }
+ var x509Certs []*x509.Certificate
+ for _, x509 := range x509s {
+ x509Cert, err := utils.ParseCertificate(x509, "PKCS7")
+ if err != nil {
+ return nil, err
+ }
+ x509Certs = append(x509Certs, x509Cert)
+ }
+ return x509Certs, nil
+}
+
+func (kw *pkcs7KeyWrapper) NoPossibleKeys(dcparameters map[string][][]byte) bool {
+ return len(kw.GetPrivateKeys(dcparameters)) == 0
+}
+
+func (kw *pkcs7KeyWrapper) GetPrivateKeys(dcparameters map[string][][]byte) [][]byte {
+ return dcparameters["privkeys"]
+}
+
+func (kw *pkcs7KeyWrapper) getPrivateKeysPasswords(dcparameters map[string][][]byte) [][]byte {
+ return dcparameters["privkeys-passwords"]
+}
+
+// UnwrapKey unwraps the symmetric key with which the layer is encrypted
+// This symmetric key is encrypted in the PKCS7 payload.
+func (kw *pkcs7KeyWrapper) UnwrapKey(dc *config.DecryptConfig, pkcs7Packet []byte) ([]byte, error) {
+ privKeys := kw.GetPrivateKeys(dc.Parameters)
+ if len(privKeys) == 0 {
+ return nil, errors.New("no private keys found for PKCS7 decryption")
+ }
+ privKeysPasswords := kw.getPrivateKeysPasswords(dc.Parameters)
+ if len(privKeysPasswords) != len(privKeys) {
+ return nil, errors.New("private key password array length must be same as that of private keys")
+ }
+
+ x509Certs, err := collectX509s(dc.Parameters["x509s"])
+ if err != nil {
+ return nil, err
+ }
+ if len(x509Certs) == 0 {
+ return nil, errors.New("no x509 certificates found needed for PKCS7 decryption")
+ }
+
+ p7, err := pkcs7.Parse(pkcs7Packet)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse PKCS7 packet: %w", err)
+ }
+
+ for idx, privKey := range privKeys {
+ key, err := utils.ParsePrivateKey(privKey, privKeysPasswords[idx], "PKCS7")
+ if err != nil {
+ return nil, err
+ }
+ for _, x509Cert := range x509Certs {
+ optsData, err := p7.Decrypt(x509Cert, crypto.PrivateKey(key))
+ if err != nil {
+ continue
+ }
+ return optsData, nil
+ }
+ }
+ return nil, errors.New("PKCS7: No suitable private key found for decryption")
+}
+
+// GetKeyIdsFromWrappedKeys converts the base64 encoded Packet to uint64 keyIds;
+// We cannot do this with pkcs7
+func (kw *pkcs7KeyWrapper) GetKeyIdsFromPacket(b64pkcs7Packets string) ([]uint64, error) {
+ return nil, nil
+}
+
+// GetRecipients converts the wrappedKeys to an array of recipients
+// We cannot do this with pkcs7
+func (kw *pkcs7KeyWrapper) GetRecipients(b64pkcs7Packets string) ([]string, error) {
+ return []string{"[pkcs7]"}, nil
+}
diff --git a/keywrap/pkcs7/keywrapper_pkcs7_test.go b/keywrap/pkcs7/keywrapper_pkcs7_test.go
new file mode 100644
index 0000000..6887e79
--- /dev/null
+++ b/keywrap/pkcs7/keywrapper_pkcs7_test.go
@@ -0,0 +1,254 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package pkcs7
+
+import (
+ "crypto/x509"
+ "testing"
+
+ "github.com/containers/ocicrypt/config"
+ "github.com/containers/ocicrypt/utils"
+)
+
+var oneEmpty []byte
+
+func createKeys() (*x509.Certificate, []byte, *x509.Certificate, []byte, error) {
+ caKey, caCert, err := utils.CreateTestCA()
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+
+ pkcs7ClientPubKey, pkcs7ClientPrivKey, err := utils.CreateRSATestKey(2048, oneEmpty, true)
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+
+ pkcs7ClientCert, err := utils.CertifyKey(pkcs7ClientPubKey, nil, caKey, caCert)
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+
+ pkcs7ClientPubKey2, pkcs7ClientPrivKey2, err := utils.CreateRSATestKey(2048, oneEmpty, true)
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+
+ pkcs7ClientCert2, err := utils.CertifyKey(pkcs7ClientPubKey2, nil, caKey, caCert)
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+
+ return pkcs7ClientCert, pkcs7ClientPrivKey, pkcs7ClientCert2, pkcs7ClientPrivKey2, nil
+}
+
+func createValidPkcs7Ccs() ([]*config.CryptoConfig, error) {
+ pkcs7ClientCert, pkcs7ClientPrivKey, pkcs7ClientCert2, pkcs7ClientPrivKey2, err := createKeys()
+ if err != nil {
+ return nil, err
+ }
+
+ validPkcs7Ccs := []*config.CryptoConfig{
+ // Client key 1
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "x509s": {pkcs7ClientCert.Raw},
+ },
+ DecryptConfig: config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {pkcs7ClientPrivKey},
+ "privkeys-passwords": {oneEmpty},
+ "x509s": {pkcs7ClientCert.Raw},
+ },
+ },
+ },
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {pkcs7ClientPrivKey},
+ "privkeys-passwords": {oneEmpty},
+ "x509s": {pkcs7ClientCert.Raw},
+ },
+ },
+ },
+
+ // Client key 2
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "x509s": {pkcs7ClientCert2.Raw},
+ },
+ DecryptConfig: config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {pkcs7ClientPrivKey2},
+ "privkeys-passwords": {oneEmpty},
+ "x509s": {pkcs7ClientCert2.Raw},
+ },
+ },
+ },
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {pkcs7ClientPrivKey2},
+ "privkeys-passwords": {oneEmpty},
+ "x509s": {pkcs7ClientCert2.Raw},
+ },
+ },
+ },
+
+ // Client key 1 without enc private key
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "x509s": {pkcs7ClientCert.Raw},
+ },
+ },
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {pkcs7ClientPrivKey},
+ "privkeys-passwords": {oneEmpty},
+ "x509s": {pkcs7ClientCert.Raw},
+ },
+ },
+ },
+
+ // Client key 2 without enc private key
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "x509s": {pkcs7ClientCert2.Raw},
+ },
+ },
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {pkcs7ClientPrivKey2},
+ "privkeys-passwords": {oneEmpty},
+ "x509s": {pkcs7ClientCert2.Raw},
+ },
+ },
+ },
+ }
+ return validPkcs7Ccs, nil
+}
+
+func createInvalidPkcs7Ccs() ([]*config.CryptoConfig, error) {
+ pkcs7ClientCert, pkcs7ClientPrivKey, pkcs7ClientCert2, pkcs7ClientPrivKey2, err := createKeys()
+ if err != nil {
+ return nil, err
+ }
+
+ invalidPkcs7Ccs := []*config.CryptoConfig{
+ // Client key 1 public with client 2 private decrypt
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "x509s": {pkcs7ClientCert.Raw},
+ },
+ },
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {pkcs7ClientPrivKey2},
+ "privkeys-passwords": {oneEmpty},
+ "x509s": {pkcs7ClientCert2.Raw},
+ },
+ },
+ },
+
+ // Client key 1 public with no private key
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "x509s": {pkcs7ClientCert.Raw},
+ },
+ },
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{},
+ },
+ },
+
+ // Invalid Client key 1 private key
+ {
+ EncryptConfig: &config.EncryptConfig{
+ Parameters: map[string][][]byte{
+ "x509s": {pkcs7ClientPrivKey},
+ },
+ },
+ DecryptConfig: &config.DecryptConfig{
+ Parameters: map[string][][]byte{
+ "privkeys": {pkcs7ClientCert.Raw},
+ "privkeys-passwords": {oneEmpty},
+ "x509s": {pkcs7ClientCert.Raw},
+ },
+ },
+ },
+ }
+ return invalidPkcs7Ccs, nil
+}
+
+func TestKeyWrapPkcs7Success(t *testing.T) {
+ validPkcs7Ccs, err := createValidPkcs7Ccs()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, cc := range validPkcs7Ccs {
+ kw := NewKeyWrapper()
+
+ data := []byte("This is some secret text")
+
+ wk, err := kw.WrapKeys(cc.EncryptConfig, data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(data) != string(ud) {
+ t.Fatal("Strings don't match")
+ }
+ }
+}
+
+func TestKeyWrapPkcs7Invalid(t *testing.T) {
+ invalidPkcs7Ccs, err := createInvalidPkcs7Ccs()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, cc := range invalidPkcs7Ccs {
+ kw := NewKeyWrapper()
+
+ data := []byte("This is some secret text")
+
+ wk, err := kw.WrapKeys(cc.EncryptConfig, data)
+ if err != nil {
+ return
+ }
+
+ ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
+ if err != nil {
+ return
+ }
+
+ if string(data) != string(ud) {
+ return
+ }
+
+ t.Fatal("Successfully wrap for invalid crypto config")
+ }
+}
diff --git a/reader.go b/reader.go
new file mode 100644
index 0000000..a93eec8
--- /dev/null
+++ b/reader.go
@@ -0,0 +1,40 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ocicrypt
+
+import (
+ "io"
+)
+
+type readerAtReader struct {
+ r io.ReaderAt
+ off int64
+}
+
+// ReaderFromReaderAt takes an io.ReaderAt and returns an io.Reader
+func ReaderFromReaderAt(r io.ReaderAt) io.Reader {
+ return &readerAtReader{
+ r: r,
+ off: 0,
+ }
+}
+
+func (rar *readerAtReader) Read(p []byte) (n int, err error) {
+ n, err = rar.r.ReadAt(p, rar.off)
+ rar.off += int64(n)
+ return n, err
+}
diff --git a/scripts/softhsm_setup b/scripts/softhsm_setup
new file mode 100755
index 0000000..3a4c0e7
--- /dev/null
+++ b/scripts/softhsm_setup
@@ -0,0 +1,287 @@
+#!/usr/bin/env bash
+
+# For the license, see the LICENSE file in the root directory.
+
+# This script may not work with softhsm2 2.0.0 but with >= 2.2.0
+
+if [ -z "$(type -P p11tool)" ]; then
+ echo "Need p11tool from gnutls"
+ exit 77
+fi
+
+if [ -z "$(type -P softhsm2-util)" ]; then
+ echo "Need softhsm2-util from softhsm2 package"
+ exit 77
+fi
+
+NAME=ocicrypt-test
+PIN=${PIN:-1234}
+SO_PIN=${SO_PIN:-1234}
+SOFTHSM_SETUP_CONFIGDIR=${SOFTHSM_SETUP_CONFIGDIR:-~/.config/softhsm2}
+export SOFTHSM2_CONF=${SOFTHSM_SETUP_CONFIGDIR}/softhsm2.conf
+
+UNAME_S="$(uname -s)"
+
+case "${UNAME_S}" in
+Darwin)
+ msg=$(sudo -v -n)
+ if [ $? -ne 0 ]; then
+ echo "Need password-less sudo rights on OS X to change /etc/gnutls/pkcs11.conf"
+ exit 1
+ fi
+ ;;
+esac
+
+teardown_softhsm() {
+ local configdir=${SOFTHSM_SETUP_CONFIGDIR}
+ local configfile=${SOFTHSM2_CONF}
+ local bakconfigfile=${configfile}.bak
+ local tokendir=${configdir}/tokens
+
+ softhsm2-util --token "${NAME}" --delete-token &>/dev/null
+
+ case "${UNAME_S}" in
+ Darwin*)
+ if [ -f /etc/gnutls/pkcs11.conf.bak ]; then
+ sudo rm -f /etc/gnutls/pkcs11.conf
+ sudo mv /etc/gnutls/pkcs11.conf.bak \
+ /etc/gnutls/pkcs11.conf &>/dev/null
+ fi
+ ;;
+ esac
+
+ if [ -f "$bakconfigfile" ]; then
+ mv "$bakconfigfile" "$configfile"
+ else
+ rm -f "$configfile"
+ fi
+ if [ -d "$tokendir" ]; then
+ rm -rf "${tokendir}"
+ fi
+ return 0
+}
+
+setup_softhsm() {
+ local msg tokenuri keyuri
+ local configdir=${SOFTHSM_SETUP_CONFIGDIR}
+ local configfile=${SOFTHSM2_CONF}
+ local bakconfigfile=${configfile}.bak
+ local tokendir=${configdir}/tokens
+ local rc
+
+ case "${UNAME_S}" in
+ Darwin*)
+ if [ -f /etc/gnutls/pkcs11.conf.bak ]; then
+ echo "/etc/gnutls/pkcs11.conf.bak already exists; need to 'teardown' first"
+ return 1
+ fi
+ sudo mv /etc/gnutls/pkcs11.conf \
+ /etc/gnutls/pkcs11.conf.bak &>/dev/null
+ if [ $(id -u) -eq 0 ]; then
+ SONAME="$(sudo -u nobody brew ls --verbose softhsm | \
+ grep -E "\.so$")"
+ else
+ SONAME="$(brew ls --verbose softhsm | \
+ grep -E "\.so$")"
+ fi
+ sudo mkdir -p /etc/gnutls &>/dev/null
+ sudo bash -c "echo "load=${SONAME}" > /etc/gnutls/pkcs11.conf"
+ ;;
+ esac
+
+ if ! [ -d $configdir ]; then
+ mkdir -p $configdir
+ fi
+ mkdir -p ${tokendir}
+
+ if [ -f $configfile ]; then
+ mv "$configfile" "$bakconfigfile"
+ fi
+
+ if ! [ -f $configfile ]; then
+ cat <<_EOF_ > $configfile
+directories.tokendir = ${tokendir}
+objectstore.backend = file
+log.level = DEBUG
+slots.removable = false
+_EOF_
+ fi
+
+ msg=$(p11tool --list-tokens 2>&1 | grep "token=${NAME}" | tail -n1)
+ if [ $? -ne 0 ]; then
+ echo "Could not list existing tokens"
+ echo "$msg"
+ fi
+ tokenuri=$(echo "$msg" | sed -n 's/.*URL: \([[:print:]*]\)/\1/p')
+
+ if [ -z "$tokenuri" ]; then
+ msg=$(softhsm2-util \
+ --init-token --pin ${PIN} --so-pin ${SO_PIN} \
+ --free --label ${NAME} 2>&1)
+ if [ $? -ne 0 ]; then
+ echo "Could not initialize token"
+ echo "$msg"
+ return 2
+ fi
+
+ slot=$(echo "$msg" | \
+ sed -n 's/.* reassigned to slot \([0-9]*\)$/\1/p')
+ if [ -z "$slot" ]; then
+ slot=$(softhsm2-util --show-slots | \
+ grep -E "^Slot " | head -n1 |
+ sed -n 's/Slot \([0-9]*\)/\1/p')
+ if [ -z "$slot" ]; then
+ echo "Could not parse slot number from output."
+ echo "$msg"
+ return 3
+ fi
+ fi
+
+ msg=$(p11tool --list-tokens 2>&1 | \
+ grep "token=${NAME}" | tail -n1)
+ if [ $? -ne 0 ]; then
+ echo "Could not list existing tokens"
+ echo "$msg"
+ fi
+ tokenuri=$(echo "$msg" | sed -n 's/.*URL: \([[:print:]*]\)/\1/p')
+ if [ -z "${tokenuri}" ]; then
+ echo "Could not get tokenuri!"
+ return 4
+ fi
+
+ # more recent versions of p11tool have --generate-privkey ...
+ msg=$(GNUTLS_PIN=$PIN p11tool \
+ --generate-privkey=rsa --label mykey --login \
+ "${tokenuri}" 2>&1)
+ if [ $? -ne 0 ]; then
+ # ... older versions have --generate-rsa
+ msg=$(GNUTLS_PIN=$PIN p11tool \
+ --generate-rsa --label mykey --login \
+ "${tokenuri}" 2>&1)
+ if [ $? -ne 0 ]; then
+ echo "Could not create RSA key!"
+ echo "$msg"
+ return 5
+ fi
+ fi
+ fi
+
+ getkeyuri_softhsm $slot
+ rc=$?
+ if [ $rc -ne 0 ]; then
+ teardown_softhsm
+ fi
+
+ return $rc
+}
+
+_getkeyuri_softhsm() {
+ local msg tokenuri keyuri
+
+ msg=$(p11tool --list-tokens 2>&1 | grep "token=${NAME}")
+ if [ $? -ne 0 ]; then
+ echo "Could not list existing tokens"
+ echo "$msg"
+ return 5
+ fi
+ tokenuri=$(echo "$msg" | sed -n 's/.*URL: \([[:print:]*]\)/\1/p')
+ if [ -z "$tokenuri" ]; then
+ echo "Could not get token URL"
+ echo "$msg"
+ return 6
+ fi
+ msg=$(p11tool --list-all ${tokenuri} 2>&1)
+ if [ $? -ne 0 ]; then
+ echo "Could not list object under token $tokenuri"
+ echo "$msg"
+ softhsm2-util --show-slots
+ return 7
+ fi
+
+ keyuri=$(echo "$msg" | sed -n 's/.*URL: \([[:print:]*]\)/\1/p')
+ if [ -z "$keyuri" ]; then
+ echo "Could not get key URL"
+ echo "$msg"
+ return 8
+ fi
+ echo "$keyuri"
+ return 0
+}
+
+getkeyuri_softhsm() {
+ local keyuri rc
+
+ keyuri=$(_getkeyuri_softhsm)
+ rc=$?
+ if [ $rc -ne 0 ]; then
+ return $rc
+ fi
+ echo "keyuri: $keyuri?pin-value=${PIN}&module-name=softhsm2"
+ return 0
+}
+
+getpubkey_softhsm() {
+ local keyuri rc
+
+ keyuri=$(_getkeyuri_softhsm)
+ rc=$?
+ if [ $rc -ne 0 ]; then
+ return $rc
+ fi
+ GNUTLS_PIN=${PIN} p11tool --export-pubkey "${keyuri}" --login 2>/dev/null
+ return $?
+}
+
+usage() {
+ cat <<_EOF_
+Usage: $0 [command]
+
+Supported commands are:
+
+setup : Setup the user's account for softhsm and create a
+ token and key with a test configuration
+
+getkeyuri : Get the key's URL; may only be called after setup
+
+getpubkey : Get the public key in PEM format; may only be called after setup
+
+teardown : Remove the temporary softhsm test configuration
+
+_EOF_
+}
+
+main() {
+ local ret
+
+ if [ $# -lt 1 ]; then
+ usage $0
+ echo -e "Missing command.\n\n"
+ return 1
+ fi
+ case "$1" in
+ setup)
+ setup_softhsm
+ ret=$?
+ ;;
+ getkeyuri)
+ getkeyuri_softhsm
+ ret=$?
+ ;;
+ getpubkey)
+ getpubkey_softhsm
+ ret=$?
+ ;;
+ teardown)
+ teardown_softhsm
+ ret=$?
+ ;;
+ *)
+ echo -e "Unsupported command: $1\n\n"
+ usage $0
+ ret=1
+ esac
+ return $ret
+}
+
+main "$@"
+exit $?
diff --git a/spec/spec.go b/spec/spec.go
new file mode 100644
index 0000000..c0c1718
--- /dev/null
+++ b/spec/spec.go
@@ -0,0 +1,20 @@
+package spec
+
+const (
+ // MediaTypeLayerEnc is MIME type used for encrypted layers.
+ MediaTypeLayerEnc = "application/vnd.oci.image.layer.v1.tar+encrypted"
+ // MediaTypeLayerGzipEnc is MIME type used for encrypted gzip-compressed layers.
+ MediaTypeLayerGzipEnc = "application/vnd.oci.image.layer.v1.tar+gzip+encrypted"
+ // MediaTypeLayerZstdEnc is MIME type used for encrypted zstd-compressed layers.
+ MediaTypeLayerZstdEnc = "application/vnd.oci.image.layer.v1.tar+zstd+encrypted"
+ // MediaTypeLayerNonDistributableEnc is MIME type used for non distributable encrypted layers.
+ MediaTypeLayerNonDistributableEnc = "application/vnd.oci.image.layer.nondistributable.v1.tar+encrypted"
+ // MediaTypeLayerNonDistributableGzipEnc is MIME type used for non distributable encrypted gzip-compressed layers.
+ MediaTypeLayerNonDistributableGzipEnc = "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip+encrypted"
+ // MediaTypeLayerNonDistributableZstdEnc is MIME type used for non distributable encrypted zstd-compressed layers.
+ MediaTypeLayerNonDistributableZstdEnc = "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd+encrypted"
+ // MediaTypeLayerNonDistributableZsdtEnc is MIME type used for non distributable encrypted zstd-compressed layers.
+ //
+ // Deprecated: Use [MediaTypeLayerNonDistributableZstdEnc].
+ MediaTypeLayerNonDistributableZsdtEnc = MediaTypeLayerNonDistributableZstdEnc
+)
diff --git a/utils/delayedreader.go b/utils/delayedreader.go
new file mode 100644
index 0000000..3b939bd
--- /dev/null
+++ b/utils/delayedreader.go
@@ -0,0 +1,109 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package utils
+
+import (
+ "io"
+)
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
+// DelayedReader wraps a io.Reader and allows a client to use the Reader
+// interface. The DelayedReader holds back some buffer to the client
+// so that it can report any error that occurred on the Reader it wraps
+// early to the client while it may still have held some data back.
+type DelayedReader struct {
+ reader io.Reader // Reader to Read() bytes from and delay them
+ err error // error that occurred on the reader
+ buffer []byte // delay buffer
+ bufbytes int // number of bytes in the delay buffer to give to Read(); on '0' we return 'EOF' to caller
+ bufoff int // offset in the delay buffer to give to Read()
+}
+
+// NewDelayedReader wraps a io.Reader and allocates a delay buffer of bufsize bytes
+func NewDelayedReader(reader io.Reader, bufsize uint) io.Reader {
+ return &DelayedReader{
+ reader: reader,
+ buffer: make([]byte, bufsize),
+ }
+}
+
+// Read implements the io.Reader interface
+func (dr *DelayedReader) Read(p []byte) (int, error) {
+ if dr.err != nil && dr.err != io.EOF {
+ return 0, dr.err
+ }
+
+ // if we are completely drained, return io.EOF
+ if dr.err == io.EOF && dr.bufbytes == 0 {
+ return 0, io.EOF
+ }
+
+ // only at the beginning we fill our delay buffer in an extra step
+ if dr.bufbytes < len(dr.buffer) && dr.err == nil {
+ dr.bufbytes, dr.err = FillBuffer(dr.reader, dr.buffer)
+ if dr.err != nil && dr.err != io.EOF {
+ return 0, dr.err
+ }
+ }
+ // dr.err != nil means we have EOF and can drain the delay buffer
+ // otherwise we need to still read from the reader
+
+ var tmpbuf []byte
+ tmpbufbytes := 0
+ if dr.err == nil {
+ tmpbuf = make([]byte, len(p))
+ tmpbufbytes, dr.err = FillBuffer(dr.reader, tmpbuf)
+ if dr.err != nil && dr.err != io.EOF {
+ return 0, dr.err
+ }
+ }
+
+ // copy out of the delay buffer into 'p'
+ tocopy1 := min(len(p), dr.bufbytes)
+ c1 := copy(p[:tocopy1], dr.buffer[dr.bufoff:])
+ dr.bufoff += c1
+ dr.bufbytes -= c1
+
+ c2 := 0
+ // can p still hold more data?
+ if c1 < len(p) {
+ // copy out of the tmpbuf into 'p'
+ c2 = copy(p[tocopy1:], tmpbuf[:tmpbufbytes])
+ }
+
+ // if tmpbuf holds data we need to hold onto, copy them
+ // into the delay buffer
+ if tmpbufbytes-c2 > 0 {
+ // left-shift the delay buffer and append the tmpbuf's remaining data
+ dr.buffer = dr.buffer[dr.bufoff : dr.bufoff+dr.bufbytes]
+ dr.buffer = append(dr.buffer, tmpbuf[c2:tmpbufbytes]...)
+ dr.bufoff = 0
+ dr.bufbytes = len(dr.buffer)
+ }
+
+ var err error
+ if dr.bufbytes == 0 {
+ err = io.EOF
+ }
+ return c1 + c2, err
+}
diff --git a/utils/delayedreader_test.go b/utils/delayedreader_test.go
new file mode 100644
index 0000000..0554574
--- /dev/null
+++ b/utils/delayedreader_test.go
@@ -0,0 +1,72 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package utils
+
+import (
+ "bytes"
+ "io"
+ "reflect"
+ "testing"
+)
+
+func makeRangeExp(n int) []int {
+ var res []int
+ for i := 0; i < n; i++ {
+ res = append(res, 1<<uint(i))
+ }
+ return res
+}
+
+func makeRange(lo, hi int) []int {
+ var res []int
+ for i := lo; i < hi; i++ {
+ res = append(res, i)
+ }
+ return res
+}
+
+func TestDelayedReader(t *testing.T) {
+ buf := make([]byte, 10)
+
+ for _, buflen := range makeRangeExp(20) {
+ obuf := make([]byte, buflen)
+
+ for _, bufsize := range makeRange(2, 32) {
+ r := bytes.NewReader(obuf)
+
+ dr := NewDelayedReader(r, uint(bufsize))
+
+ var ibuf []byte
+ for {
+ n, err := dr.Read(buf)
+ if n == 0 {
+ t.Fatal("Did not expect n == 0")
+ }
+ if err != nil && err != io.EOF {
+ t.Fatal(err)
+ }
+ ibuf = append(ibuf, buf[:n]...)
+ if err == io.EOF {
+ break
+ }
+ }
+ if !reflect.DeepEqual(ibuf, obuf) {
+ t.Fatalf("original buffer (len=%d) != received buffer (len=%d)", len(obuf), len(ibuf))
+ }
+ }
+ }
+}
diff --git a/utils/ioutils.go b/utils/ioutils.go
new file mode 100644
index 0000000..c626516
--- /dev/null
+++ b/utils/ioutils.go
@@ -0,0 +1,58 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package utils
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "os/exec"
+)
+
+// FillBuffer fills the given buffer with as many bytes from the reader as possible. It returns
+// EOF if an EOF was encountered or any other error.
+func FillBuffer(reader io.Reader, buffer []byte) (int, error) {
+ n, err := io.ReadFull(reader, buffer)
+ if err == io.ErrUnexpectedEOF {
+ return n, io.EOF
+ }
+ return n, err
+}
+
+// first argument is the command, like cat or echo,
+// the second is the list of args to pass to it
+type CommandExecuter interface {
+ Exec(string, []string, []byte) ([]byte, error)
+}
+
+type Runner struct{}
+
+// ExecuteCommand is used to execute a linux command line command and return the output of the command with an error if it exists.
+func (r Runner) Exec(cmdName string, args []string, input []byte) ([]byte, error) {
+ var out bytes.Buffer
+ var stderr bytes.Buffer
+ stdInputBuffer := bytes.NewBuffer(input)
+ cmd := exec.Command(cmdName, args...)
+ cmd.Stdin = stdInputBuffer
+ cmd.Stdout = &out
+ cmd.Stderr = &stderr
+ err := cmd.Run()
+ if err != nil {
+ return nil, fmt.Errorf("Error while running command: %s. stderr: %s: %w", cmdName, stderr.String(), err)
+ }
+ return out.Bytes(), nil
+}
diff --git a/utils/keyprovider/keyprovider.pb.go b/utils/keyprovider/keyprovider.pb.go
new file mode 100644
index 0000000..dc477d3
--- /dev/null
+++ b/utils/keyprovider/keyprovider.pb.go
@@ -0,0 +1,243 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: keyprovider.proto
+
+package keyprovider
+
+import (
+ context "context"
+ fmt "fmt"
+ proto "github.com/golang/protobuf/proto"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+ math "math"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
+
+type KeyProviderKeyWrapProtocolInput struct {
+ KeyProviderKeyWrapProtocolInput []byte `protobuf:"bytes,1,opt,name=KeyProviderKeyWrapProtocolInput,proto3" json:"KeyProviderKeyWrapProtocolInput,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *KeyProviderKeyWrapProtocolInput) Reset() { *m = KeyProviderKeyWrapProtocolInput{} }
+func (m *KeyProviderKeyWrapProtocolInput) String() string { return proto.CompactTextString(m) }
+func (*KeyProviderKeyWrapProtocolInput) ProtoMessage() {}
+func (*KeyProviderKeyWrapProtocolInput) Descriptor() ([]byte, []int) {
+ return fileDescriptor_da74c8e785ad390c, []int{0}
+}
+
+func (m *KeyProviderKeyWrapProtocolInput) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_KeyProviderKeyWrapProtocolInput.Unmarshal(m, b)
+}
+func (m *KeyProviderKeyWrapProtocolInput) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_KeyProviderKeyWrapProtocolInput.Marshal(b, m, deterministic)
+}
+func (m *KeyProviderKeyWrapProtocolInput) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_KeyProviderKeyWrapProtocolInput.Merge(m, src)
+}
+func (m *KeyProviderKeyWrapProtocolInput) XXX_Size() int {
+ return xxx_messageInfo_KeyProviderKeyWrapProtocolInput.Size(m)
+}
+func (m *KeyProviderKeyWrapProtocolInput) XXX_DiscardUnknown() {
+ xxx_messageInfo_KeyProviderKeyWrapProtocolInput.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_KeyProviderKeyWrapProtocolInput proto.InternalMessageInfo
+
+func (m *KeyProviderKeyWrapProtocolInput) GetKeyProviderKeyWrapProtocolInput() []byte {
+ if m != nil {
+ return m.KeyProviderKeyWrapProtocolInput
+ }
+ return nil
+}
+
+type KeyProviderKeyWrapProtocolOutput struct {
+ KeyProviderKeyWrapProtocolOutput []byte `protobuf:"bytes,1,opt,name=KeyProviderKeyWrapProtocolOutput,proto3" json:"KeyProviderKeyWrapProtocolOutput,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *KeyProviderKeyWrapProtocolOutput) Reset() { *m = KeyProviderKeyWrapProtocolOutput{} }
+func (m *KeyProviderKeyWrapProtocolOutput) String() string { return proto.CompactTextString(m) }
+func (*KeyProviderKeyWrapProtocolOutput) ProtoMessage() {}
+func (*KeyProviderKeyWrapProtocolOutput) Descriptor() ([]byte, []int) {
+ return fileDescriptor_da74c8e785ad390c, []int{1}
+}
+
+func (m *KeyProviderKeyWrapProtocolOutput) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_KeyProviderKeyWrapProtocolOutput.Unmarshal(m, b)
+}
+func (m *KeyProviderKeyWrapProtocolOutput) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_KeyProviderKeyWrapProtocolOutput.Marshal(b, m, deterministic)
+}
+func (m *KeyProviderKeyWrapProtocolOutput) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_KeyProviderKeyWrapProtocolOutput.Merge(m, src)
+}
+func (m *KeyProviderKeyWrapProtocolOutput) XXX_Size() int {
+ return xxx_messageInfo_KeyProviderKeyWrapProtocolOutput.Size(m)
+}
+func (m *KeyProviderKeyWrapProtocolOutput) XXX_DiscardUnknown() {
+ xxx_messageInfo_KeyProviderKeyWrapProtocolOutput.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_KeyProviderKeyWrapProtocolOutput proto.InternalMessageInfo
+
+func (m *KeyProviderKeyWrapProtocolOutput) GetKeyProviderKeyWrapProtocolOutput() []byte {
+ if m != nil {
+ return m.KeyProviderKeyWrapProtocolOutput
+ }
+ return nil
+}
+
+func init() {
+ proto.RegisterType((*KeyProviderKeyWrapProtocolInput)(nil), "keyprovider.keyProviderKeyWrapProtocolInput")
+ proto.RegisterType((*KeyProviderKeyWrapProtocolOutput)(nil), "keyprovider.keyProviderKeyWrapProtocolOutput")
+}
+
+func init() {
+ proto.RegisterFile("keyprovider.proto", fileDescriptor_da74c8e785ad390c)
+}
+
+var fileDescriptor_da74c8e785ad390c = []byte{
+ // 169 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcc, 0x4e, 0xad, 0x2c,
+ 0x28, 0xca, 0x2f, 0xcb, 0x4c, 0x49, 0x2d, 0xd2, 0x03, 0x32, 0x4a, 0xf2, 0x85, 0xb8, 0x91, 0x84,
+ 0x94, 0xb2, 0xb9, 0xe4, 0x81, 0xdc, 0x00, 0x28, 0xd7, 0x3b, 0xb5, 0x32, 0xbc, 0x28, 0xb1, 0x20,
+ 0x00, 0xa4, 0x2e, 0x39, 0x3f, 0xc7, 0x33, 0xaf, 0xa0, 0xb4, 0x44, 0xc8, 0x83, 0x4b, 0xde, 0x1b,
+ 0xbf, 0x12, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x9e, 0x20, 0x42, 0xca, 0x94, 0xf2, 0xb8, 0x14, 0x70,
+ 0x5b, 0xe6, 0x5f, 0x5a, 0x02, 0xb2, 0xcd, 0x8b, 0x4b, 0xc1, 0x9b, 0x80, 0x1a, 0xa8, 0x75, 0x04,
+ 0xd5, 0x19, 0xbd, 0x62, 0xe4, 0x12, 0x42, 0x52, 0x14, 0x9c, 0x5a, 0x54, 0x96, 0x99, 0x9c, 0x2a,
+ 0x94, 0xc1, 0xc5, 0x0e, 0x52, 0x0c, 0x94, 0x11, 0xd2, 0xd1, 0x43, 0x0e, 0x1f, 0x02, 0x21, 0x21,
+ 0xa5, 0x4b, 0xa4, 0x6a, 0x88, 0xf5, 0x4a, 0x0c, 0x42, 0x59, 0x5c, 0x9c, 0xa1, 0x79, 0xf4, 0xb1,
+ 0xcb, 0x89, 0x37, 0x0a, 0x39, 0x62, 0x93, 0xd8, 0xc0, 0x91, 0x6d, 0x0c, 0x08, 0x00, 0x00, 0xff,
+ 0xff, 0x9a, 0x10, 0xcb, 0xf9, 0x01, 0x02, 0x00, 0x00,
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConnInterface
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion6
+
+// KeyProviderServiceClient is the client API for KeyProviderService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
+type KeyProviderServiceClient interface {
+ WrapKey(ctx context.Context, in *KeyProviderKeyWrapProtocolInput, opts ...grpc.CallOption) (*KeyProviderKeyWrapProtocolOutput, error)
+ UnWrapKey(ctx context.Context, in *KeyProviderKeyWrapProtocolInput, opts ...grpc.CallOption) (*KeyProviderKeyWrapProtocolOutput, error)
+}
+
+type keyProviderServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewKeyProviderServiceClient(cc grpc.ClientConnInterface) KeyProviderServiceClient {
+ return &keyProviderServiceClient{cc}
+}
+
+func (c *keyProviderServiceClient) WrapKey(ctx context.Context, in *KeyProviderKeyWrapProtocolInput, opts ...grpc.CallOption) (*KeyProviderKeyWrapProtocolOutput, error) {
+ out := new(KeyProviderKeyWrapProtocolOutput)
+ err := c.cc.Invoke(ctx, "/keyprovider.KeyProviderService/WrapKey", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *keyProviderServiceClient) UnWrapKey(ctx context.Context, in *KeyProviderKeyWrapProtocolInput, opts ...grpc.CallOption) (*KeyProviderKeyWrapProtocolOutput, error) {
+ out := new(KeyProviderKeyWrapProtocolOutput)
+ err := c.cc.Invoke(ctx, "/keyprovider.KeyProviderService/UnWrapKey", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// KeyProviderServiceServer is the server API for KeyProviderService service.
+type KeyProviderServiceServer interface {
+ WrapKey(context.Context, *KeyProviderKeyWrapProtocolInput) (*KeyProviderKeyWrapProtocolOutput, error)
+ UnWrapKey(context.Context, *KeyProviderKeyWrapProtocolInput) (*KeyProviderKeyWrapProtocolOutput, error)
+}
+
+// UnimplementedKeyProviderServiceServer can be embedded to have forward compatible implementations.
+type UnimplementedKeyProviderServiceServer struct {
+}
+
+func (*UnimplementedKeyProviderServiceServer) WrapKey(ctx context.Context, req *KeyProviderKeyWrapProtocolInput) (*KeyProviderKeyWrapProtocolOutput, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method WrapKey not implemented")
+}
+func (*UnimplementedKeyProviderServiceServer) UnWrapKey(ctx context.Context, req *KeyProviderKeyWrapProtocolInput) (*KeyProviderKeyWrapProtocolOutput, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method UnWrapKey not implemented")
+}
+
+func RegisterKeyProviderServiceServer(s *grpc.Server, srv KeyProviderServiceServer) {
+ s.RegisterService(&_KeyProviderService_serviceDesc, srv)
+}
+
+func _KeyProviderService_WrapKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(KeyProviderKeyWrapProtocolInput)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(KeyProviderServiceServer).WrapKey(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/keyprovider.KeyProviderService/WrapKey",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(KeyProviderServiceServer).WrapKey(ctx, req.(*KeyProviderKeyWrapProtocolInput))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _KeyProviderService_UnWrapKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(KeyProviderKeyWrapProtocolInput)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(KeyProviderServiceServer).UnWrapKey(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/keyprovider.KeyProviderService/UnWrapKey",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(KeyProviderServiceServer).UnWrapKey(ctx, req.(*KeyProviderKeyWrapProtocolInput))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+var _KeyProviderService_serviceDesc = grpc.ServiceDesc{
+ ServiceName: "keyprovider.KeyProviderService",
+ HandlerType: (*KeyProviderServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "WrapKey",
+ Handler: _KeyProviderService_WrapKey_Handler,
+ },
+ {
+ MethodName: "UnWrapKey",
+ Handler: _KeyProviderService_UnWrapKey_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "keyprovider.proto",
+}
diff --git a/utils/keyprovider/keyprovider.proto b/utils/keyprovider/keyprovider.proto
new file mode 100644
index 0000000..a71f0a5
--- /dev/null
+++ b/utils/keyprovider/keyprovider.proto
@@ -0,0 +1,17 @@
+syntax = "proto3";
+
+package keyprovider;
+option go_package = "keyprovider";
+
+message keyProviderKeyWrapProtocolInput {
+ bytes KeyProviderKeyWrapProtocolInput = 1;
+}
+
+message keyProviderKeyWrapProtocolOutput {
+ bytes KeyProviderKeyWrapProtocolOutput = 1;
+}
+
+service KeyProviderService {
+ rpc WrapKey(keyProviderKeyWrapProtocolInput) returns (keyProviderKeyWrapProtocolOutput) {};
+ rpc UnWrapKey(keyProviderKeyWrapProtocolInput) returns (keyProviderKeyWrapProtocolOutput) {};
+} \ No newline at end of file
diff --git a/utils/softhsm/softhsm.go b/utils/softhsm/softhsm.go
new file mode 100644
index 0000000..cc97a1e
--- /dev/null
+++ b/utils/softhsm/softhsm.go
@@ -0,0 +1,91 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package softhsm
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+)
+
+type SoftHSMSetup struct {
+ statedir string
+}
+
+func NewSoftHSMSetup() *SoftHSMSetup {
+ return &SoftHSMSetup{}
+}
+
+// GetConfigFilename returns the path to the softhsm configuration file; this function
+// may only be called after RunSoftHSMSetup
+func (s *SoftHSMSetup) GetConfigFilename() string {
+ return s.statedir + "/softhsm2.conf"
+}
+
+// RunSoftHSMSetup runs 'softhsm_setup setup' and returns the public key that was displayed
+func (s *SoftHSMSetup) RunSoftHSMSetup(softhsmSetup string) (string, error) {
+ statedir, err := os.MkdirTemp("", "ocicrypt")
+ if err != nil {
+ return "", fmt.Errorf("Could not create temporary directory fot softhsm state: %w", err)
+ }
+ s.statedir = statedir
+
+ cmd := exec.Command(softhsmSetup, "setup")
+ var out bytes.Buffer
+ cmd.Stdout = &out
+ cmd.Env = append(cmd.Env, "SOFTHSM_SETUP_CONFIGDIR="+s.statedir)
+ err = cmd.Run()
+ if err != nil {
+ os.RemoveAll(s.statedir)
+ return "", fmt.Errorf("%s setup failed: %s: %w", softhsmSetup, out.String(), err)
+ }
+
+ o := out.String()
+ idx := strings.Index(o, "pkcs11:")
+ if idx < 0 {
+ os.RemoveAll(s.statedir)
+ return "", errors.New("Could not find pkcs11 URI in output")
+ }
+
+ return strings.TrimRight(o[idx:], "\n "), nil
+}
+
+// RunSoftHSMGetPubkey runs 'softhsm_setup getpubkey' and returns the public key
+func (s *SoftHSMSetup) RunSoftHSMGetPubkey(softhsmSetup string) (string, error) {
+ cmd := exec.Command(softhsmSetup, "getpubkey")
+ var out bytes.Buffer
+ cmd.Stdout = &out
+ cmd.Env = append(cmd.Env, "SOFTHSM_SETUP_CONFIGDIR="+s.statedir)
+ err := cmd.Run()
+ if err != nil {
+ return "", fmt.Errorf("%s getpubkey failed: %s: %w", softhsmSetup, out.String(), err)
+ }
+
+ return out.String(), nil
+}
+
+// RunSoftHSMTeardown runs 'softhsm_setup teardown
+func (s *SoftHSMSetup) RunSoftHSMTeardown(softhsmSetup string) {
+ cmd := exec.Command(softhsmSetup, "teardown")
+ cmd.Env = append(cmd.Env, "SOFTHSM_SETUP_CONFIGDIR="+s.statedir)
+ _ = cmd.Run()
+
+ os.RemoveAll(s.statedir)
+}
diff --git a/utils/testing.go b/utils/testing.go
new file mode 100644
index 0000000..69bb9d1
--- /dev/null
+++ b/utils/testing.go
@@ -0,0 +1,165 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package utils
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "fmt"
+ "math/big"
+ "time"
+)
+
+// CreateRSAKey creates an RSA key
+func CreateRSAKey(bits int) (*rsa.PrivateKey, error) {
+ key, err := rsa.GenerateKey(rand.Reader, bits)
+ if err != nil {
+ return nil, fmt.Errorf("rsa.GenerateKey failed: %w", err)
+ }
+ return key, nil
+}
+
+// CreateRSATestKey creates an RSA key of the given size and returns
+// the public and private key in PEM or DER format
+func CreateRSATestKey(bits int, password []byte, pemencode bool) ([]byte, []byte, error) {
+ key, err := CreateRSAKey(bits)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ pubData, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
+ if err != nil {
+ return nil, nil, fmt.Errorf("x509.MarshalPKIXPublicKey failed: %w", err)
+ }
+ privData := x509.MarshalPKCS1PrivateKey(key)
+
+ // no more encoding needed for DER
+ if !pemencode {
+ return pubData, privData, nil
+ }
+
+ publicKey := pem.EncodeToMemory(&pem.Block{
+ Type: "PUBLIC KEY",
+ Bytes: pubData,
+ })
+
+ var block *pem.Block
+
+ typ := "RSA PRIVATE KEY"
+ if len(password) > 0 {
+ block, err = x509.EncryptPEMBlock(rand.Reader, typ, privData, password, x509.PEMCipherAES256) //nolint:staticcheck // ignore SA1019, which is kept for backward compatibility
+ if err != nil {
+ return nil, nil, fmt.Errorf("x509.EncryptPEMBlock failed: %w", err)
+ }
+ } else {
+ block = &pem.Block{
+ Type: typ,
+ Bytes: privData,
+ }
+ }
+
+ privateKey := pem.EncodeToMemory(block)
+
+ return publicKey, privateKey, nil
+}
+
+// CreateECDSATestKey creates and elliptic curve key for the given curve and returns
+// the public and private key in DER format
+func CreateECDSATestKey(curve elliptic.Curve) ([]byte, []byte, error) {
+ key, err := ecdsa.GenerateKey(curve, rand.Reader)
+ if err != nil {
+ return nil, nil, fmt.Errorf("ecdsa.GenerateKey failed: %w", err)
+ }
+
+ pubData, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
+ if err != nil {
+ return nil, nil, fmt.Errorf("x509.MarshalPKIXPublicKey failed: %w", err)
+ }
+
+ privData, err := x509.MarshalECPrivateKey(key)
+ if err != nil {
+ return nil, nil, fmt.Errorf("x509.MarshalECPrivateKey failed: %w", err)
+ }
+
+ return pubData, privData, nil
+}
+
+// CreateTestCA creates a root CA for testing
+func CreateTestCA() (*rsa.PrivateKey, *x509.Certificate, error) {
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return nil, nil, fmt.Errorf("rsa.GenerateKey failed: %w", err)
+ }
+
+ ca := &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{
+ CommonName: "test-ca",
+ },
+ NotBefore: time.Now(),
+ NotAfter: time.Now().AddDate(1, 0, 0),
+ IsCA: true,
+ KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+ BasicConstraintsValid: true,
+ }
+ caCert, err := certifyKey(&key.PublicKey, ca, key, ca)
+
+ return key, caCert, err
+}
+
+// CertifyKey certifies a public key using the given CA's private key and cert;
+// The certificate template for the public key is optional
+func CertifyKey(pubbytes []byte, template *x509.Certificate, caKey *rsa.PrivateKey, caCert *x509.Certificate) (*x509.Certificate, error) {
+ pubKey, err := ParsePublicKey(pubbytes, "CertifyKey")
+ if err != nil {
+ return nil, err
+ }
+ return certifyKey(pubKey, template, caKey, caCert)
+}
+
+func certifyKey(pub interface{}, template *x509.Certificate, caKey *rsa.PrivateKey, caCert *x509.Certificate) (*x509.Certificate, error) {
+ if template == nil {
+ template = &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{
+ CommonName: "testkey",
+ },
+ NotBefore: time.Now(),
+ NotAfter: time.Now().Add(time.Hour),
+ IsCA: false,
+ KeyUsage: x509.KeyUsageDigitalSignature,
+ BasicConstraintsValid: true,
+ }
+ }
+
+ certDER, err := x509.CreateCertificate(rand.Reader, template, caCert, pub, caKey)
+ if err != nil {
+ return nil, fmt.Errorf("x509.CreateCertificate failed: %w", err)
+ }
+
+ cert, err := x509.ParseCertificate(certDER)
+ if err != nil {
+ return nil, fmt.Errorf("x509.ParseCertificate failed: %w", err)
+ }
+
+ return cert, nil
+}
diff --git a/utils/utils.go b/utils/utils.go
new file mode 100644
index 0000000..160f747
--- /dev/null
+++ b/utils/utils.go
@@ -0,0 +1,249 @@
+/*
+ Copyright The ocicrypt Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package utils
+
+import (
+ "bytes"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/containers/ocicrypt/crypto/pkcs11"
+ "github.com/go-jose/go-jose/v3"
+ "golang.org/x/crypto/openpgp"
+)
+
+// parseJWKPrivateKey parses the input byte array as a JWK and makes sure it's a private key
+func parseJWKPrivateKey(privKey []byte, prefix string) (interface{}, error) {
+ jwk := jose.JSONWebKey{}
+ err := jwk.UnmarshalJSON(privKey)
+ if err != nil {
+ return nil, fmt.Errorf("%s: Could not parse input as JWK: %w", prefix, err)
+ }
+ if jwk.IsPublic() {
+ return nil, fmt.Errorf("%s: JWK is not a private key", prefix)
+ }
+ return &jwk, nil
+}
+
+// parseJWKPublicKey parses the input byte array as a JWK
+func parseJWKPublicKey(privKey []byte, prefix string) (interface{}, error) {
+ jwk := jose.JSONWebKey{}
+ err := jwk.UnmarshalJSON(privKey)
+ if err != nil {
+ return nil, fmt.Errorf("%s: Could not parse input as JWK: %w", prefix, err)
+ }
+ if !jwk.IsPublic() {
+ return nil, fmt.Errorf("%s: JWK is not a public key", prefix)
+ }
+ return &jwk, nil
+}
+
+// parsePkcs11PrivateKeyYaml parses the input byte array as pkcs11 key file yaml format)
+func parsePkcs11PrivateKeyYaml(yaml []byte, prefix string) (*pkcs11.Pkcs11KeyFileObject, error) {
+ // if the URI does not have enough attributes, we will throw an error when decrypting
+ return pkcs11.ParsePkcs11KeyFile(yaml)
+}
+
+// parsePkcs11URIPublicKey parses the input byte array as a pkcs11 key file yaml
+func parsePkcs11PublicKeyYaml(yaml []byte, prefix string) (*pkcs11.Pkcs11KeyFileObject, error) {
+ // if the URI does not have enough attributes, we will throw an error when decrypting
+ return pkcs11.ParsePkcs11KeyFile(yaml)
+}
+
+// IsPasswordError checks whether an error is related to a missing or wrong
+// password
+func IsPasswordError(err error) bool {
+ if err == nil {
+ return false
+ }
+ msg := strings.ToLower(err.Error())
+
+ return strings.Contains(msg, "password") &&
+ (strings.Contains(msg, "missing") || strings.Contains(msg, "wrong"))
+}
+
+// ParsePrivateKey tries to parse a private key in DER format first and
+// PEM format after, returning an error if the parsing failed
+func ParsePrivateKey(privKey, privKeyPassword []byte, prefix string) (interface{}, error) {
+ key, err := x509.ParsePKCS8PrivateKey(privKey)
+ if err != nil {
+ key, err = x509.ParsePKCS1PrivateKey(privKey)
+ if err != nil {
+ key, err = x509.ParseECPrivateKey(privKey)
+ }
+ }
+ if err != nil {
+ block, _ := pem.Decode(privKey)
+ if block != nil {
+ var der []byte
+ if x509.IsEncryptedPEMBlock(block) { //nolint:staticcheck // ignore SA1019, which is kept for backward compatibility
+ if privKeyPassword == nil {
+ return nil, fmt.Errorf("%s: Missing password for encrypted private key", prefix)
+ }
+ der, err = x509.DecryptPEMBlock(block, privKeyPassword) //nolint:staticcheck // ignore SA1019, which is kept for backward compatibility
+ if err != nil {
+ return nil, fmt.Errorf("%s: Wrong password: could not decrypt private key", prefix)
+ }
+ } else {
+ der = block.Bytes
+ }
+
+ key, err = x509.ParsePKCS8PrivateKey(der)
+ if err != nil {
+ key, err = x509.ParsePKCS1PrivateKey(der)
+ if err != nil {
+ return nil, fmt.Errorf("%s: Could not parse private key: %w", prefix, err)
+ }
+ }
+ } else {
+ key, err = parseJWKPrivateKey(privKey, prefix)
+ if err != nil {
+ key, err = parsePkcs11PrivateKeyYaml(privKey, prefix)
+ }
+ }
+ }
+ return key, err
+}
+
+// IsPrivateKey returns true in case the given byte array represents a private key
+// It returns an error if for example the password is wrong
+func IsPrivateKey(data []byte, password []byte) (bool, error) {
+ _, err := ParsePrivateKey(data, password, "")
+ return err == nil, err
+}
+
+// IsPkcs11PrivateKey returns true in case the given byte array represents a pkcs11 private key
+func IsPkcs11PrivateKey(data []byte) bool {
+ return pkcs11.IsPkcs11PrivateKey(data)
+}
+
+// ParsePublicKey tries to parse a public key in DER format first and
+// PEM format after, returning an error if the parsing failed
+func ParsePublicKey(pubKey []byte, prefix string) (interface{}, error) {
+ key, err := x509.ParsePKIXPublicKey(pubKey)
+ if err != nil {
+ block, _ := pem.Decode(pubKey)
+ if block != nil {
+ key, err = x509.ParsePKIXPublicKey(block.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("%s: Could not parse public key: %w", prefix, err)
+ }
+ } else {
+ key, err = parseJWKPublicKey(pubKey, prefix)
+ if err != nil {
+ key, err = parsePkcs11PublicKeyYaml(pubKey, prefix)
+ }
+ }
+ }
+ return key, err
+}
+
+// IsPublicKey returns true in case the given byte array represents a public key
+func IsPublicKey(data []byte) bool {
+ _, err := ParsePublicKey(data, "")
+ return err == nil
+}
+
+// IsPkcs11PublicKey returns true in case the given byte array represents a pkcs11 public key
+func IsPkcs11PublicKey(data []byte) bool {
+ return pkcs11.IsPkcs11PublicKey(data)
+}
+
+// ParseCertificate tries to parse a public key in DER format first and
+// PEM format after, returning an error if the parsing failed
+func ParseCertificate(certBytes []byte, prefix string) (*x509.Certificate, error) {
+ x509Cert, err := x509.ParseCertificate(certBytes)
+ if err != nil {
+ block, _ := pem.Decode(certBytes)
+ if block == nil {
+ return nil, fmt.Errorf("%s: Could not PEM decode x509 certificate", prefix)
+ }
+ x509Cert, err = x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("%s: Could not parse x509 certificate: %w", prefix, err)
+ }
+ }
+ return x509Cert, err
+}
+
+// IsCertificate returns true in case the given byte array represents an x.509 certificate
+func IsCertificate(data []byte) bool {
+ _, err := ParseCertificate(data, "")
+ return err == nil
+}
+
+// IsGPGPrivateKeyRing returns true in case the given byte array represents a GPG private key ring file
+func IsGPGPrivateKeyRing(data []byte) bool {
+ r := bytes.NewBuffer(data)
+ _, err := openpgp.ReadKeyRing(r)
+ return err == nil
+}
+
+// SortDecryptionKeys parses a list of comma separated base64 entries and sorts the data into
+// a map. Each entry in the list may be either a GPG private key ring, private key, or x.509
+// certificate
+func SortDecryptionKeys(b64ItemList string) (map[string][][]byte, error) {
+ dcparameters := make(map[string][][]byte)
+
+ for _, b64Item := range strings.Split(b64ItemList, ",") {
+ var password []byte
+ b64Data := strings.Split(b64Item, ":")
+ keyData, err := base64.StdEncoding.DecodeString(b64Data[0])
+ if err != nil {
+ return nil, errors.New("Could not base64 decode a passed decryption key")
+ }
+ if len(b64Data) == 2 {
+ password, err = base64.StdEncoding.DecodeString(b64Data[1])
+ if err != nil {
+ return nil, errors.New("Could not base64 decode a passed decryption key password")
+ }
+ }
+ var key string
+ isPrivKey, err := IsPrivateKey(keyData, password)
+ if IsPasswordError(err) {
+ return nil, err
+ }
+ if isPrivKey {
+ key = "privkeys"
+ if _, ok := dcparameters["privkeys-passwords"]; !ok {
+ dcparameters["privkeys-passwords"] = [][]byte{password}
+ } else {
+ dcparameters["privkeys-passwords"] = append(dcparameters["privkeys-passwords"], password)
+ }
+ } else if IsCertificate(keyData) {
+ key = "x509s"
+ } else if IsGPGPrivateKeyRing(keyData) {
+ key = "gpg-privatekeys"
+ }
+ if key != "" {
+ values := dcparameters[key]
+ if values == nil {
+ dcparameters[key] = [][]byte{keyData}
+ } else {
+ dcparameters[key] = append(dcparameters[key], keyData)
+ }
+ } else {
+ return nil, errors.New("Unknown decryption key type")
+ }
+ }
+
+ return dcparameters, nil
+}