summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 18:13:12 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 18:13:12 +0000
commit15c19b31f86e4cf770ae0f1e1f0c1888fe74f6d9 (patch)
tree0dbcaecb926d28f706ac7f9d41cc1a50ec99a153
parentInitial commit. (diff)
downloadgolang-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--.gitignore2
-rw-r--r--.travis.yml25
-rw-r--r--LICENSE177
-rw-r--r--Makefile28
-rw-r--r--README.md102
-rw-r--r--pkcs11uri.go453
-rw-r--r--pkcs11uri_test.go338
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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..49cc83d
--- /dev/null
+++ b/LICENSE
@@ -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)
+ }
+}