diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 18:13:12 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 18:13:12 +0000 |
commit | 15c19b31f86e4cf770ae0f1e1f0c1888fe74f6d9 (patch) | |
tree | 0dbcaecb926d28f706ac7f9d41cc1a50ec99a153 | |
parent | Initial commit. (diff) | |
download | golang-github-stefanberger-go-pkcs11uri-15c19b31f86e4cf770ae0f1e1f0c1888fe74f6d9.tar.xz golang-github-stefanberger-go-pkcs11uri-15c19b31f86e4cf770ae0f1e1f0c1888fe74f6d9.zip |
Adding upstream version 0.0~git20201008.78d3cae.upstream/0.0_git20201008.78d3caeupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | .travis.yml | 25 | ||||
-rw-r--r-- | LICENSE | 177 | ||||
-rw-r--r-- | Makefile | 28 | ||||
-rw-r--r-- | README.md | 102 | ||||
-rw-r--r-- | pkcs11uri.go | 453 | ||||
-rw-r--r-- | pkcs11uri_test.go | 338 |
7 files changed, 1125 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1823f5d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*~ +pkcs11uri diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f5f274f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +dist: bionic +language: go + +os: +- linux + +go: + - "1.13.x" + +matrix: + include: + - os: linux + +addons: + apt: + packages: + - softhsm2 + +install: + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.30.0 + +script: + - make + - make check + - make test @@ -0,0 +1,177 @@ + + 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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1a10515 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +# Copyright IBM Corporation, 2020 + +# 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 test + +all: build + +FORCE: + +check: + golangci-lint run + +build: + go build ./... + +test: + go test ./... -test.v diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1fc6e9 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# go-pkcs11uri + +Welcome to the go-pkcs11uri library. The implementation follows [RFC 7512](https://tools.ietf.org/html/rfc7512) and this [errata](https://www.rfc-editor.org/errata/rfc7512). + +# Exampe usage: + +The following example builds on this library [here](https://github.com/miekg/pkcs11) and are using softhsm2 on Fedora. + +## Example + +This example program extending the one found [here](https://github.com/miekg/pkcs11/blob/master/README.md#examples): + +``` +package main + +import ( + "fmt" + "os" + "strconv" + + "github.com/miekg/pkcs11" + pkcs11uri "github.com/stefanberger/go-pkcs11uri" +) + +func main() { + if len(os.Args) < 2 { + panic("Missing pkcs11 URI argument") + } + uristr := os.Args[1] + + uri, err := pkcs11uri.New() + if err != nil { + panic(err) + } + err = uri.Parse(uristr) + if err != nil { + panic(err) + } + + module, err := uri.GetModule() + if err != nil { + panic(err) + } + + slot, ok := uri.GetPathAttribute("slot-id", false) + if !ok { + panic("No slot-id in pkcs11 URI") + } + slotid, err := strconv.Atoi(slot) + if err != nil { + panic(err) + } + + pin, err := uri.GetPIN() + if err != nil { + panic(err) + } + + p := pkcs11.New(module) + err = p.Initialize() + if err != nil { + panic(err) + } + + defer p.Destroy() + defer p.Finalize() + + session, err := p.OpenSession(uint(slotid), pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION) + if err != nil { + panic(err) + } + defer p.CloseSession(session) + + err = p.Login(session, pkcs11.CKU_USER, pin) + if err != nil { + panic(err) + } + defer p.Logout(session) + + p.DigestInit(session, []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_SHA_1, nil)}) + hash, err := p.Digest(session, []byte("this is a string")) + if err != nil { + panic(err) + } + + for _, d := range hash { + fmt.Printf("%x", d) + } + fmt.Println() +} +``` + +## Exampe Usage + +``` +$ sudo softhsm2-util --init-token --slot 1 --label test --pin 1234 --so-pin 1234 +The token has been initialized and is reassigned to slot 2053753261 +$ go build ./... +$ sudo ./pkcs11-example 'pkcs11:slot-id=2053753261?module-path=/usr/lib64/pkcs11/libsofthsm2.so&pin-value=1234' +517592df8fec3ad146a79a9af153db2a4d784ec5 +``` + diff --git a/pkcs11uri.go b/pkcs11uri.go new file mode 100644 index 0000000..39b0654 --- /dev/null +++ b/pkcs11uri.go @@ -0,0 +1,453 @@ +/* + (c) Copyright IBM Corporation, 2020 + + 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 pkcs11uri + +import ( + "errors" + "fmt" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +// Pkcs11URI holds a pkcs11 URI object +type Pkcs11URI struct { + // path and query attributes may have custom attributes that either + // have to be in the query or in the path part, so we use two maps + pathAttributes map[string]string + queryAttributes map[string]string + // directories to search for pkcs11 modules + moduleDirectories []string + // file paths of allowed pkcs11 modules + allowedModulePaths []string + // whether any module is allowed to be loaded + allowAnyModule bool + // A map of environment variables needed by the pkcs11 module using this URI. + // This map is not needed by this implementation but is there for convenience. + env map[string]string +} + +// upper character hex digits needed for pct-encoding +const hex = "0123456789ABCDEF" + +// escapeAll pct-escapes all characters in the string +func escapeAll(s string) string { + res := make([]byte, len(s)*3) + j := 0 + for i := 0; i < len(s); i++ { + c := s[i] + res[j] = '%' + res[j+1] = hex[c>>4] + res[j+2] = hex[c&0xf] + j += 3 + } + return string(res) +} + +// escape pct-escapes the path and query part of the pkcs11 URI following the different rules of the +// path and query part as decribed in RFC 7512 sec. 2.3 +func escape(s string, isPath bool) string { + res := make([]byte, len(s)*3) + j := 0 + for i := 0; i < len(s); i++ { + c := s[i] + // unreserved per RFC 3986 sec. 2.3 + if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') { + res[j] = c + } else if isPath && c == '&' { + res[j] = c + } else if !isPath && (c == '/' || c == '?' || c == '|') { + res[j] = c + } else { + switch c { + case '-', '.', '_', '~': // unreserved per RFC 3986 sec. 2.3 + res[j] = c + case ':', '[', ']', '@', '!', '$', '\'', '(', ')', '*', '+', ',', '=': + res[j] = c + default: + res[j] = '%' + res[j+1] = hex[c>>4] + res[j+2] = hex[c&0xf] + j += 2 + } + } + j++ + } + return string(res[:j]) +} + +// New creates a new Pkcs11URI object +func New() *Pkcs11URI { + return &Pkcs11URI{ + pathAttributes: make(map[string]string), + queryAttributes: make(map[string]string), + env: make(map[string]string), + } +} + +func (uri *Pkcs11URI) setAttribute(attrMap map[string]string, name, value string) error { + v, err := url.PathUnescape(value) + if err != nil { + return err + } + attrMap[name] = v + return nil +} + +// GetPathAttribute returns the value of a path attribute in unescaped form or +// pct-encoded form +func (uri *Pkcs11URI) GetPathAttribute(name string, pctencode bool) (string, bool) { + v, ok := uri.pathAttributes[name] + if ok && pctencode { + v = escape(v, true) + } + return v, ok +} + +// SetPathAttribute sets the value for a path attribute; this function may return an error +// if the given value cannot be pct-unescaped +func (uri *Pkcs11URI) SetPathAttribute(name, value string) error { + return uri.setAttribute(uri.pathAttributes, name, value) +} + +// AddPathAttribute adds a path attribute; it returns an error if an attribute with the same +// name already existed or if the given value cannot be pct-unescaped +func (uri *Pkcs11URI) AddPathAttribute(name, value string) error { + if _, ok := uri.pathAttributes[name]; ok { + return errors.New("duplicate path attribute") + } + return uri.SetPathAttribute(name, value) +} + +// RemovePathAttribute removes a path attribute +func (uri *Pkcs11URI) RemovePathAttribute(name string) { + delete(uri.pathAttributes, name) +} + +// AddEnv adds an environment variable for the pkcs11 module +func (uri *Pkcs11URI) AddEnv(name, value string) { + uri.env[name] = value +} + +// SetEnvMap sets the environment variables for the pkcs11 module +func (uri *Pkcs11URI) SetEnvMap(env map[string]string) { + uri.env = env +} + +// GetEnvMap returns the map of environment variables +func (uri *Pkcs11URI) GetEnvMap() map[string]string { + return uri.env +} + +// GetQueryAttribute returns the value of a query attribute in unescaped or +// pct-encoded form +func (uri *Pkcs11URI) GetQueryAttribute(name string, pctencode bool) (string, bool) { + v, ok := uri.queryAttributes[name] + if ok && pctencode { + v = escape(v, false) + } + return v, ok +} + +// SetQueryAttribute sets the value for a query attribute; this function may return an error +// if the given value cannot pct-unescaped +func (uri *Pkcs11URI) SetQueryAttribute(name, value string) error { + return uri.setAttribute(uri.queryAttributes, name, value) +} + +// AddQueryAttribute adds a query attribute; it returns an error if an attribute with the same +// name already existed or if the given value cannot be pct-unescaped +func (uri *Pkcs11URI) AddQueryAttribute(name, value string) error { + if _, ok := uri.queryAttributes[name]; ok { + return errors.New("duplicate query attribute") + } + return uri.SetQueryAttribute(name, value) +} + +// RemoveQueryAttribute removes a path attribute +func (uri *Pkcs11URI) RemoveQueryAttribute(name string) { + delete(uri.queryAttributes, name) +} + +// Validate validates a Pkcs11URI object's attributes following RFC 7512 rules and proper formatting of +// their values +func (uri *Pkcs11URI) Validate() error { + /* RFC 7512: 2.3 */ + /* slot-id should be DIGIT, but we go for number */ + if v, ok := uri.pathAttributes["slot-id"]; ok { + if _, err := strconv.Atoi(v); err != nil { + return fmt.Errorf("slot-id must be a number: %s", v) + } + } + + /* library-version should 1*DIGIT [ "." 1 *DIGIT ]; allow NUMBERS for DIGIT */ + if v, ok := uri.pathAttributes["library-version"]; ok { + m, err := regexp.Match("^[0-9]+(\\.[0-9]+)?$", []byte(v)) + if err != nil || !m { + return fmt.Errorf("Invalid format for library-version '%s'", v) + } + } + + if v, ok := uri.pathAttributes["type"]; ok { + m, err := regexp.Match("^(public|private|cert|secret-key}data)?$", []byte(v)) + if err != nil || !m { + return fmt.Errorf("Invalid type '%s'", v) + } + } + + /* RFC 7512: 2.4 */ + _, ok1 := uri.queryAttributes["pin-source"] + _, ok2 := uri.queryAttributes["pin-value"] + if ok1 && ok2 { + return errors.New("URI must not contain pin-source and pin-value") + } + + if v, ok := uri.queryAttributes["module-path"]; ok { + if !filepath.IsAbs(v) { + return fmt.Errorf("path %s of module-name attribute must be absolute", v) + } + } + + return nil +} + +// HasPIN allows the user to check whether a PIN has been provided either by the pin-value or the pin-source +// attributes. It should be called before GetPIN(), which may still fail getting the PIN from a file for example. +func (uri *Pkcs11URI) HasPIN() bool { + _, ok := uri.queryAttributes["pin-value"] + if ok { + return true + } + _, ok = uri.queryAttributes["pin-source"] + return ok +} + +// GetPIN gets the PIN from either the pin-value or pin-source attribute; a user may want to call HasPIN() +// before calling this function to determine whether a PIN has been provided at all so that an error code +// returned by this function indicates that the PIN value could not be retrieved. +func (uri *Pkcs11URI) GetPIN() (string, error) { + if v, ok := uri.queryAttributes["pin-value"]; ok { + return v, nil + } + if v, ok := uri.queryAttributes["pin-source"]; ok { + pinuri, err := url.ParseRequestURI(v) + if err != nil { + return "", fmt.Errorf("Could not parse pin-source: %s ", err) + } + switch pinuri.Scheme { + case "", "file": + if !filepath.IsAbs(pinuri.Path) { + return "", fmt.Errorf("PIN URI path '%s' is not absolute", pinuri.Path) + } + pin, err := ioutil.ReadFile(pinuri.Path) + if err != nil { + return "", fmt.Errorf("Could not open PIN file: %s", err) + } + return string(pin), nil + default: + return "", fmt.Errorf("PIN URI scheme %s is not supported", pinuri.Scheme) + } + } + return "", fmt.Errorf("Neither pin-source nor pin-value are available") +} + +// Parse parses a pkcs11: URI string +func (uri *Pkcs11URI) Parse(uristring string) error { + if !strings.HasPrefix(uristring, "pkcs11:") { + return errors.New("Malformed pkcs11 URI: missing pcks11: prefix") + } + + parts := strings.SplitN(uristring[7:], "?", 2) + + uri.pathAttributes = make(map[string]string) + uri.queryAttributes = make(map[string]string) + + if len(parts[0]) > 0 { + /* parse path part */ + for _, part := range strings.Split(parts[0], ";") { + p := strings.SplitN(part, "=", 2) + if len(p) != 2 { + return errors.New("Malformed pkcs11 URI: malformed path attribute") + } + if err := uri.AddPathAttribute(p[0], p[1]); err != nil { + return fmt.Errorf("Malformed pkcs11 URI: %s", err) + } + } + } + + if len(parts) == 2 { + /* parse query part */ + for _, part := range strings.Split(parts[1], "&") { + p := strings.SplitN(part, "=", 2) + if len(p) != 2 { + return errors.New("Malformed pkcs11 URI: malformed query attribute") + } + if err := uri.AddQueryAttribute(p[0], p[1]); err != nil { + return fmt.Errorf("Malformed pkcs11 URI: %s", err) + } + } + } + return uri.Validate() +} + +// formatAttribute formats attributes and escapes their values as needed +func formatAttributes(attrMap map[string]string, ispath bool) string { + res := "" + for key, value := range attrMap { + switch key { + case "id": + /* id is always pct-encoded */ + value = escapeAll(value) + default: + if ispath { + value = escape(value, true) + } else { + value = escape(value, false) + } + } + if len(res) > 0 { + if ispath { + res += ";" + } else { + res += "&" + } + } + res += key + "=" + value + } + return res +} + +// Format formats a Pkcs11URI to it string representaion +func (uri *Pkcs11URI) Format() (string, error) { + if err := uri.Validate(); err != nil { + return "", err + } + result := "pkcs11:" + formatAttributes(uri.pathAttributes, true) + if len(uri.queryAttributes) > 0 { + result += "?" + formatAttributes(uri.queryAttributes, false) + } + return result, nil +} + +// SetModuleDirectories sets the search directories for pkcs11 modules +func (uri *Pkcs11URI) SetModuleDirectories(moduleDirectories []string) { + uri.moduleDirectories = moduleDirectories +} + +// GetModuleDirectories gets the search directories for pkcs11 modules +func (uri *Pkcs11URI) GetModuleDirectories() []string { + return uri.moduleDirectories +} + +// SetAllowedModulePaths sets allowed module paths to restrict access to modules. +// Directory entries must end with a '/', all other ones are assumed to be file entries. +// Allowed modules are filtered by string matching. +func (uri *Pkcs11URI) SetAllowedModulePaths(allowedModulePaths []string) { + uri.allowedModulePaths = allowedModulePaths +} + +// SetAllowAnyModule allows any module to be loaded; by default this is not allowed +func (uri *Pkcs11URI) SetAllowAnyModule(allowAnyModule bool) { + uri.allowAnyModule = allowAnyModule +} + +func (uri *Pkcs11URI) isAllowedPath(path string, allowedPaths []string) bool { + if uri.allowAnyModule { + return true + } + for _, allowedPath := range allowedPaths { + if allowedPath == path { + // exact filename match + return true + } + if allowedPath[len(allowedPath)-1] == '/' && strings.HasPrefix(path, allowedPath) { + // allowedPath no subdirectory is allowed + idx := strings.IndexRune(path[len(allowedPath):], os.PathSeparator) + if idx < 0 { + return true + } + } + } + return false +} + +// GetModule returns the module to use or an error in case no module could be found. +// First the module-path is checked for whether it holds an absolute that can be read +// by the current user. If this is the case the module is returned. Otherwise either the module-path +// is used or the user-provided module path is used to match a module containing what is set in the +// attribute module-name. +func (uri *Pkcs11URI) GetModule() (string, error) { + var searchdirs []string + v, ok := uri.queryAttributes["module-path"] + + if ok { + info, err := os.Stat(v) + if err != nil { + return "", fmt.Errorf("module-path '%s' is not accessible", v) + } + if err == nil && info.Mode().IsRegular() { + // it's a file + if uri.isAllowedPath(v, uri.allowedModulePaths) { + return v, nil + } + return "", fmt.Errorf("module-path '%s' is not allowed by policy", v) + } + if !info.IsDir() { + return "", fmt.Errorf("module-path '%s' points to an invalid file type", v) + } + // v is a directory + searchdirs = []string{v} + } else { + searchdirs = uri.GetModuleDirectories() + } + + moduleName, ok := uri.queryAttributes["module-name"] + if !ok { + return "", fmt.Errorf("module-name attribute is not set") + } + moduleName = strings.ToLower(moduleName) + + for _, dir := range searchdirs { + files, err := ioutil.ReadDir(dir) + if err != nil { + continue + } + for _, file := range files { + fileLower := strings.ToLower(file.Name()) + + i := strings.Index(fileLower, moduleName) + if i < 0 { + continue + } + // we require that the fileLower ends with moduleName or that + // a suffix follows so that softhsm will not match libsofthsm2.so but only + // libsofthsm.so + if len(fileLower) == i+len(moduleName) || fileLower[i+len(moduleName)] == '.' { + f := filepath.Join(dir, file.Name()) + if uri.isAllowedPath(f, uri.allowedModulePaths) { + return f, nil + } + return "", fmt.Errorf("module '%s' is not allowed by policy", f) + } + } + } + return "", fmt.Errorf("No module could be found") +} diff --git a/pkcs11uri_test.go b/pkcs11uri_test.go new file mode 100644 index 0000000..ac34eeb --- /dev/null +++ b/pkcs11uri_test.go @@ -0,0 +1,338 @@ +/* + (c) Copyright IBM Corporation, 2020 + + 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 pkcs11uri + +import ( + "fmt" + "io/ioutil" + "os" + "testing" +) + +var modulePaths = []string{ + "/usr/lib64/pkcs11/", // Fedora, RHEL, openSUSE + "/usr/lib/pkcs11/", // Fedora 32 bit, ArchLinux + "/usr/lib/softhsm/", // Ubuntu, Debian, Alpine +} + +func TestParse1(t *testing.T) { + uri := New() + + original := "pkcs11:id=%02;object=SIGN%20pubkey;token=SSH%20key;manufacturer=piv_II?module-path=/usr/lib64/pkcs11/opensc-pkcs11.so" + err := uri.Parse(original) + + if err != nil { + t.Fatalf("Could not parse URI: %s", err) + } + + for _, attr := range []string{"id", "object", "token", "manufacturer"} { + if _, ok := uri.GetPathAttribute(attr, false); !ok { + t.Fatalf("Path attribute %s is not available", attr) + } + } + for _, attr := range []string{"module-path"} { + if _, ok := uri.GetQueryAttribute(attr, false); !ok { + t.Fatalf("Query attribute %s is not available", attr) + } + } + + _, err = uri.Format() + if err != nil { + t.Fatalf("Could not format the uri: %s", err) + } +} + +func verifyURI(t *testing.T, uri *Pkcs11URI, expecteduri string) { + encoded, err := uri.Format() + if err != nil { + t.Fatalf("Could not format the uri: %s", err) + } + if encoded != expecteduri { + t.Fatalf("Did not get expected URI '%s' but '%s'", expecteduri, encoded) + } +} + +func verifyPIN(t *testing.T, uri *Pkcs11URI, expectedpin string) { + if !uri.HasPIN() { + t.Fatalf("HasPIN indicated that the URI does not have a PIN") + } + pin, err := uri.GetPIN() + if err != nil { + t.Fatalf("Could not get PIN: %s", err) + } + if pin != expectedpin { + t.Fatalf("Did not get expected PIN value of '1234' but '%s'", pin) + } +} + +func TestConstruct1(t *testing.T) { + uri := New() + expecteduri := "pkcs11:id=%66%6F%6F" + + err := uri.AddPathAttribute("id", "%66oo") + if err != nil { + t.Fatalf("Could not add path attribute: %s", err) + } + + verifyURI(t, uri, expecteduri) + + expectedpin := "1234" + expecteduri += fmt.Sprintf("?pin-value=%s", expectedpin) + + err = uri.AddQueryAttribute("pin-value", expectedpin) + if err != nil { + t.Fatalf("Could not add query attribute: %s", err) + } + + verifyURI(t, uri, expecteduri) + verifyPIN(t, uri, expectedpin) +} + +func writeTempfile(t *testing.T, value string) *os.File { + tmpfile, err := ioutil.TempFile("", "mypin") + if err != nil { + t.Fatalf("Coult not create temporary file: %s", err) + } + if _, err := tmpfile.Write([]byte(value)); err != nil { + t.Fatalf("Could not write to tempfile: %s", err) + } + if err := tmpfile.Close(); err != nil { + t.Fatalf("Could not close tempfile: %s", err) + } + return tmpfile +} + +func TestPinSource(t *testing.T) { + uri := New() + expectedpin := "4321" + + tmpfile := writeTempfile(t, expectedpin) + defer os.Remove(tmpfile.Name()) + + expecteduri := "pkcs11:id=%66%6F%6F?pin-source=file:" + tmpfile.Name() + err := uri.AddPathAttribute("id", "foo") + if err != nil { + t.Fatalf("Could not add path attribute: %s", err) + } + err = uri.AddQueryAttribute("pin-source", "file:"+tmpfile.Name()) + if err != nil { + t.Fatalf("Could not add query attribute: %s", err) + } + + verifyURI(t, uri, expecteduri) + verifyPIN(t, uri, expectedpin) + + expecteduri = "pkcs11:id=%66%6F%6F?pin-source=" + tmpfile.Name() + + uri.RemoveQueryAttribute("pin-source") + err = uri.AddQueryAttribute("pin-source", tmpfile.Name()) + if err != nil { + t.Fatalf("Could not add query attribute: %s", err) + } + + verifyURI(t, uri, expecteduri) + verifyPIN(t, uri, expectedpin) +} + +func TestBadInput(t *testing.T) { + uri := New() + + for _, entry := range [][]string{{"slot-id", "foo"}, {"library-version", "foo"}, {"library-version", "1.bar"}, {"type", "fobbar"}} { + err := uri.AddPathAttribute(entry[0], entry[1]) + if err != nil { + t.Fatalf("Could not add path attribute: %s", err) + } + + if err := uri.Validate(); err == nil { + t.Fatalf("uri validation should have failed due to malformed %s value '%s'", entry[0], entry[1]) + } + uri.RemovePathAttribute(entry[0]) + } +} + +func TestGoodInput(t *testing.T) { + uri := New() + + for _, entry := range [][]string{{"slot-id", "1"}, {"library-version", "7"}, {"library-version", "1.8"}, {"type", "public"}} { + err := uri.AddPathAttribute(entry[0], entry[1]) + if err != nil { + t.Fatalf("Could not add path attribute: %s", err) + } + + if err := uri.Validate(); err != nil { + t.Fatalf("uri validation should have succeeded for %s value '%s': %s", entry[0], entry[1], err) + } + uri.RemovePathAttribute(entry[0]) + } +} + +func TestURIs(t *testing.T) { + uri := New() + uris := []string{ + "pkcs11:", + "pkcs11:object=my-pubkey;type=public", + "pkcs11:object=my-key;type=private?pin-source=file:/etc/token", + "pkcs11:token=The%20Software%20PKCS%2311%20Softtoken;manufacturer=Snake%20Oil,%20Inc.;model=1.0;object=my-certificate;type=cert;id=%69%95%3E%5C%F4%BD%EC%91;serial=?pin-source=file:/etc/token_pin", + "pkcs11:object=my-sign-key;type=private?module-name=mypkcs11", + "pkcs11:object=my-sign-key;type=private?module-path=/mnt/libmypkcs11.so.1", + "pkcs11:token=Software%20PKCS%2311%20softtoken;manufacturer=Snake%20Oil,%20Inc.?pin-value=the-pin", + "pkcs11:slot-description=Sun%20Metaslot", + "pkcs11:library-manufacturer=Snake%20Oil,%20Inc.;library-description=Soft%20Token%20Library;library-version=1.23", + "pkcs11:token=My%20token%25%20created%20by%20Joe;library-version=3;id=%01%02%03%Ba%dd%Ca%fe%04%05%06", + "pkcs11:token=A%20name%20with%20a%20substring%20%25%3B;object=my-certificate;type=cert", + "pkcs11:token=Name%20with%20a%20small%20A%20with%20acute:%20%C3%A1;object=my-certificate;type=cert", + "pkcs11:token=my-token;object=my-certificate;type=cert;vendor-aaa=value-a?pin-source=file:/etc/token_pin&vendor-bbb=value-b", + } + for _, uristring := range uris { + err := uri.Parse(uristring) + if err != nil { + t.Fatalf("Could not parse URI '%s': %s", uristring, err) + } + + encoded, err := uri.Format() + if err != nil { + t.Fatalf("Could not format URI '%s': %s", uristring, err) + } + // the order of attributes may be different but the string lengths are the same + if len(encoded) != len(uristring) { + t.Fatalf("String lengths are different: '%s' vs. '%s'", encoded, uristring) + } + } +} + +func TestValidateEscapedAttrs(t *testing.T) { + type data struct { + uri string + testp []string // pair of attribute and expected value in path part (unescaped, pct-encoded) + testq []string // pair of attribute and expected value in query part (unescaped, pct-encoded) + format bool // whether to format the URI and compare against given uri (equal strings) + } + input := []data{ + { + uri: "pkcs11:token=Software%20PKCS%2311%20softtoken;manufacturer=Snake%20Oil,%20Inc.?pin-value=the-pin", + testp: []string{"token", "Software PKCS#11 softtoken", "Software%20PKCS%2311%20softtoken"}, + format: false, + }, { + uri: "pkcs11:token=My%20token%25%20created%20by%20Joe;library-version=3;id=%01%02%03%Ba%dd%Ca%fe%04%05%06", + testp: []string{"token", "My token% created by Joe", "My%20token%25%20created%20by%20Joe"}, + format: false, + }, { + // test pk11-query-res-avail and pk11-path-res-avail special characters + uri: "pkcs11:token=:[]@!$'()*+,=&?attr=:[]@!$'()*+,=/?", + testp: []string{"token", ":[]@!$'()*+,=&", ":[]@!$'()*+,=&"}, + testq: []string{"attr", ":[]@!$'()*+,=/?", ":[]@!$'()*+,=/?"}, + format: true, + }, { + // test (some) unnecessarily escaped characters + uri: "pkcs11:token=%3a%5b%5d%40%21%24%27%28%29%2a%2b%2c%26%3d-%60%20%3c%3e%7b", + testp: []string{"token", ":[]@!$'()*+,&=-` <>{", ":[]@!$'()*+,&=-%60%20%3C%3E%7B"}, + format: false, + }, { + // test some non-printable characters that have to be escape; + uri: "pkcs11:token=%00%01%02Hello%FF%FE", + testp: []string{"token", "\x00\x01\x02Hello\xff\xfe", "%00%01%02Hello%FF%FE"}, + format: true, + }, + } + + uri := New() + for _, data := range input { + err := uri.Parse(data.uri) + if err != nil { + t.Fatalf("Could not parse URI '%s': %s", data.uri, err) + } + if len(data.testp[1]) > 0 { + v, _ := uri.GetPathAttribute(data.testp[0], false) + if v != data.testp[1] { + t.Fatalf("Got unexpected unescaped path attribute value '%s'; expected '%s'", v, data.testp[1]) + } + } + if len(data.testp[2]) > 0 { + v, _ := uri.GetPathAttribute(data.testp[0], true) + if v != data.testp[2] { + t.Fatalf("Got unexpected pct-encoded path attribute value '%s'; expected '%s'", v, data.testp[2]) + } + } + if len(data.testq) > 0 { + if len(data.testq[1]) > 0 { + v, _ := uri.GetQueryAttribute(data.testq[0], false) + if v != data.testq[1] { + t.Fatalf("Got unexpected unescaped query attribute value '%s'; expected '%s'", v, data.testq[1]) + } + } + if len(data.testq[2]) > 0 { + v, _ := uri.GetQueryAttribute(data.testq[0], true) + if v != data.testq[2] { + t.Fatalf("Got unexpected pct-encoded query attribute value '%s'; expected '%s'", v, data.testq[2]) + } + } + } + if data.format { + encoded, err := uri.Format() + if err != nil { + t.Fatalf("Could not format URI '%s': %s", data.uri, err) + } + if encoded != data.uri { + t.Fatalf("Formatted URI is different than expected: '%s' vs. '%s'", encoded, data.uri) + } + } + } +} + +// This test requires SoftHSM to be installed, will warn otherwise +func TestGetModule(t *testing.T) { + uri := New() + uri.SetModuleDirectories(modulePaths) + uri.SetAllowAnyModule(true) + + uristring := "pkcs11:?module-name=softhsm2" + err := uri.Parse(uristring) + if err != nil { + t.Fatalf("Could not parse pkcs11 URI '%s': %s", uristring, err) + } + + _, err = uri.GetModule() + if err != nil { + t.Skipf("Is softhsm2 not installed? GetModule() failed: %s", err) + } +} + +// This test requires SoftHSM to be installed, will warn otherwise +func TestGetModuleRestricted(t *testing.T) { + uri := New() + uri.SetModuleDirectories(modulePaths) + + uristring := "pkcs11:?module-name=softhsm2" + err := uri.Parse(uristring) + if err != nil { + t.Fatalf("Could not parse pkcs11 URI '%s': %s", uristring, err) + } + + // we don't want any results + uri.SetAllowedModulePaths([]string{"/usr"}) + _, err = uri.GetModule() + if err == nil { + t.Errorf("GetModule() must fail due to allowed file paths: %s", err) + } + + // this time we want module paths + uri.SetAllowedModulePaths(modulePaths) + _, err = uri.GetModule() + if err != nil { + t.Skipf("Is softhsm2 not installed? GetModule() failed: %s", err) + } +} |