Files

185 lines
5.6 KiB
Go

package pkcs11
import (
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
)
// DefaultModule is p11-kit's proxy, which exposes every module the system has registered,
// tpm2-pkcs11 included, so a URI without module-path works on a stock p11-kit setup.
const DefaultModule = "p11-kit-proxy.so"
// URI is the subset of an RFC 7512 PKCS#11 URI this client understands: the token label,
// the module to load and where the user PIN comes from.
type URI struct {
Token string
ModulePath string
pinValue *string
pinSource string
}
// ParseURI parses raw following RFC 7512 section 2.3. A path attribute other than token
// is refused rather than ignored: the path narrows which token is used, and ignoring a
// constraint such as serial would widen the match to whichever token is listed first,
// where the RFC calls for no match at all. Duplicate attributes are refused, a
// module-path must be absolute and a module-name must be a bare name. Unknown query
// attributes are ignored, as the RFC asks.
func ParseURI(raw string) (*URI, error) {
rest, ok := cutPrefixFold(raw, "pkcs11:")
if !ok {
return nil, errors.New("PKCS#11 URI does not start with the pkcs11 scheme")
}
path, query, _ := strings.Cut(rest, "?")
u := &URI{}
if err := eachAttribute(path, ";", u.setPathAttribute); err != nil {
return nil, err
}
if err := eachAttribute(query, "&", u.setQueryAttribute); err != nil {
return nil, err
}
// RFC 7512 section 2.4: a URI with both pin-value and pin-source should be refused.
if u.pinValue != nil && u.pinSource != "" {
return nil, errors.New("PKCS#11 URI carries both pin-value and pin-source")
}
return u, nil
}
func (u *URI) setPathAttribute(name, value string) error {
if name != "token" {
return fmt.Errorf("PKCS#11 URI path attribute %q is not supported, only token selects a token", name)
}
u.Token = value
return nil
}
func (u *URI) setQueryAttribute(name, value string) error {
switch name {
case "module-path":
if !filepath.IsAbs(value) {
return fmt.Errorf("PKCS#11 URI module-path %q must be absolute", value)
}
u.ModulePath = value
case "module-name":
if value == "" || strings.ContainsAny(value, `/\`) || strings.Contains(value, "..") {
return fmt.Errorf("PKCS#11 URI module-name %q must be a module name, not a path", value)
}
u.ModulePath = "lib" + value + ".so"
case "pin-value":
u.pinValue = &value
case "pin-source":
path, err := pinSourcePath(value)
if err != nil {
return err
}
u.pinSource = path
}
return nil
}
// pinSourcePath turns a pin-source value into the file to read. A plain path is used as
// it is; a file URI must name a local absolute path, with no host or localhost (RFC 8089
// section 2), so a PIN is never read from somewhere the URI only appears to point to.
func pinSourcePath(value string) (string, error) {
if value == "" {
return "", errors.New("PKCS#11 URI pin-source is empty")
}
rest, isFile := cutPrefixFold(value, "file:")
if !isFile {
return value, nil
}
authorityAndPath, hasAuthority := strings.CutPrefix(rest, "//")
if !hasAuthority {
if !isAbsolutePath(rest) {
return "", fmt.Errorf("PKCS#11 URI pin-source %q must be an absolute path", value)
}
return rest, nil
}
if isAbsolutePath(authorityAndPath) {
return authorityAndPath, nil
}
host, path, _ := strings.Cut(authorityAndPath, "/")
if host != "" && !strings.EqualFold(host, "localhost") {
return "", fmt.Errorf("PKCS#11 URI pin-source host %q is not the local machine", host)
}
if path == "" {
return "", fmt.Errorf("PKCS#11 URI pin-source %q names no file", value)
}
return "/" + path, nil
}
// isAbsolutePath reports whether p is absolute in a file URI. A path starting with a slash
// is absolute there on every platform, including Windows, where filepath.IsAbs wants a
// drive letter.
func isAbsolutePath(p string) bool {
return strings.HasPrefix(p, "/") || filepath.IsAbs(p)
}
// cutPrefixFold is strings.CutPrefix with the prefix matched without regard to case.
func cutPrefixFold(s, prefix string) (string, bool) {
if len(s) < len(prefix) || !strings.EqualFold(s[:len(prefix)], prefix) {
return s, false
}
return s[len(prefix):], true
}
// Module is the library to load, DefaultModule when the URI names none.
func (u *URI) Module() string {
if u.ModulePath == "" {
return DefaultModule
}
return u.ModulePath
}
// HasPIN reports whether the URI carries a PIN, inline or as a pin-source.
func (u *URI) HasPIN() bool {
return u.pinValue != nil || u.pinSource != ""
}
// PIN returns the user PIN, or nil when the URI carries none and no login should happen.
// A pin-source names a file whose single line is the PIN.
func (u *URI) PIN() ([]byte, error) {
if u.pinValue != nil {
return []byte(*u.pinValue), nil
}
if u.pinSource == "" {
return nil, nil
}
pin, err := os.ReadFile(u.pinSource)
if err != nil {
return nil, fmt.Errorf("read PIN: %w", err)
}
return []byte(strings.TrimRight(string(pin), "\r\n")), nil
}
// eachAttribute splits list on sep and calls fn for every name=value pair, refusing a
// name that appears twice.
func eachAttribute(list, sep string, fn func(name, value string) error) error {
if list == "" {
return nil
}
seen := make(map[string]struct{})
for _, pair := range strings.Split(list, sep) {
name, value, ok := strings.Cut(pair, "=")
if !ok {
return fmt.Errorf("PKCS#11 URI attribute %q has no value", pair)
}
if _, dup := seen[name]; dup {
return fmt.Errorf("PKCS#11 URI attribute %s appears more than once", name)
}
seen[name] = struct{}{}
value, err := url.PathUnescape(value)
if err != nil {
return fmt.Errorf("PKCS#11 URI attribute %s: %w", name, err)
}
if err := fn(name, value); err != nil {
return err
}
}
return nil
}