mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-19 20:09:07 +02:00
185 lines
5.6 KiB
Go
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
|
|
}
|