diff --git a/piv/internal/pcsc/pcsc.go b/piv/internal/pcsc/pcsc.go index bed8a20..1bf1967 100644 --- a/piv/internal/pcsc/pcsc.go +++ b/piv/internal/pcsc/pcsc.go @@ -12,18 +12,33 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package pcsc implements the libpcsclite protocol for communicating with pcscd. +// +// This package is a pure Go implementation of libpcsclite, allowing piv-go to +// communicate directly with pcscd without cgo. This still relies on pcscd to +// communicate with the OS, and will be less reliable than linking against the +// shared libraries provided by the pcscd packages. +// +// This package will NOT work with the native Mac and Windows libraries, which +// are provided directly by the OS. Though pcsclite can be installed on Mac. package pcsc import ( "bytes" "context" "encoding/binary" + "encoding/hex" "fmt" "io" "net" "os" ) +// pcscd messages directly encode C structs over a unix domain socket. Determine +// the host's byte order with build tags so encoding/binary can replicate this +// behavior. +// +// https://groups.google.com/g/golang-nuts/c/3GEzwKfRRQw/m/ppkJKrT4cfAJ var nativeByteOrder binary.ByteOrder const ( @@ -32,40 +47,82 @@ const ( ) const ( - scardSSuccess = 0 + // RVSuccess is a return value indicating the operation succeeded. + RVSuccess = 0 + + majorVersion = 4 + minorVersion = 4 // https://github.com/LudovicRousseau/PCSC/blob/1.9.0/src/winscard_msg.h#L76 - commandVersion = 0x11 - commandGetReadersState = 0x12 + commandEstablishContext = 0x01 + commandReleaseContext = 0x02 + commandConnect = 0x04 + commandDisconnect = 0x05 + commandBeginTransaction = 0x07 + commandEndTransaction = 0x08 + commandTransmit = 0x09 + commandVersion = 0x11 + commandGetReadersState = 0x12 + + // Context modes to be passed to NewContext. + // + // https://github.com/LudovicRousseau/PCSC/blob/1.9.0/src/PCSC/pcsclite.h.in#L248 + Exclusive = 0x0001 + Shared = 0x0002 + Direct = 0x0003 + + // Different protocols that can be used when connecting to a card. + ProtocolT0 = 0x0001 + ProtocolT1 = 0x0002 + ProtocolRaw = 0x0004 + ProtocolT15 = 0x0005 + + // Disconnect option. + LeaveCard = 0x0000 + ResetCard = 0x0001 + UnpowerCard = 0x0002 + EjectCard = 0x0003 ) +// RVError wraps an underlying PCSC error code. +type RVError struct { + RV uint32 +} + +// Error returns a string encoding of the error code. Note that human readable +// messages are not provided by this package, and are handled by the piv +// package instead. +func (r *RVError) Error() string { + return fmt.Sprintf("rv 0x%x", r.RV) +} + +// Client represents a connection with the pcscd process. type Client struct { conn net.Conn } +// Close releases the underlying connection. It does not release any contexts +// which must be closed separately. func (c *Client) Close() error { return c.conn.Close() } -func (c *Client) version() (major, minor int32, err error) { +func (c *Client) checkVersion() error { req := struct { Major int32 Minor int32 RV uint32 - }{4, 4, scardSSuccess} - - var resp struct { - Major int32 - Minor int32 - RV uint32 + }{majorVersion, minorVersion, RVSuccess} + if err := c.sendMessage(commandVersion, req, &req); err != nil { + return fmt.Errorf("send message: %v", err) } - if err := c.sendMessage(commandVersion, req, &resp); err != nil { - return 0, 0, fmt.Errorf("send message: %v", err) + if req.RV != RVSuccess { + return &RVError{RV: req.RV} } - if resp.RV != scardSSuccess { - return 0, 0, fmt.Errorf("invalid response value: %x", resp.RV) + if req.Major != majorVersion { + return fmt.Errorf("unsupported major version of pcscd protocol: %d", req.Major) } - return resp.Major, resp.Minor, nil + return nil } const ( @@ -77,25 +134,14 @@ const ( maxReaders = 16 ) -// https://github.com/LudovicRousseau/PCSC/blob/1.9.0/src/eventhandler.h#L52 -// typedef struct pubReaderStatesList -// { -// char readerName[MAX_READERNAME]; /**< reader name */ -// uint32_t eventCounter; /**< number of card events */ -// uint32_t readerState; /**< SCARD_* bit field */ -// int32_t readerSharing; /**< PCSCLITE_SHARING_* sharing status */ +// readerState holds metadata about a PCSC card. // -// UCHAR cardAtr[MAX_ATR_SIZE]; /**< ATR */ -// uint32_t cardAtrLength; /**< ATR length */ -// uint32_t cardProtocol; /**< SCARD_PROTOCOL_* value */ -// } -// READER_STATE; - +// https://github.com/LudovicRousseau/PCSC/blob/1.9.0/src/eventhandler.h#L52 type readerState struct { Name [maxReaderNameSize]byte EventCounter uint32 State uint32 - Sharing uint32 + Sharing int32 Attr [maxAttributeSize]byte AttrSize uint32 @@ -115,11 +161,11 @@ func (r readerState) name() string { return string(r.Name[:i]) } -func (c *Client) readers() ([]string, error) { - var resp [maxReaders]readerState - - if err := c.sendMessage(commandGetReadersState, nil, &resp); err != nil { - return nil, fmt.Errorf("send message: %v", err) +// Readers returns the names of all readers that are connected to the device. +func (c *Client) Readers() ([]string, error) { + resp, err := c.readers() + if err != nil { + return nil, err } var names []string @@ -132,6 +178,182 @@ func (c *Client) readers() ([]string, error) { return names, nil } +func (c *Client) readers() (states [maxReaders]readerState, err error) { + if err := c.sendMessage(commandGetReadersState, nil, &states); err != nil { + return states, fmt.Errorf("send message: %v", err) + } + return states, nil +} + +// https://github.com/LudovicRousseau/PCSC/blob/1.9.0/src/winscard_msg.h#L118 +type establishRequest struct { + Scope uint32 + Context uint32 + RV uint32 +} + +// Context holds an open PCSC context, which is required to perform actions +// such as starting transactions or transmitting data to a card. +type Context struct { + client *Client + context uint32 +} + +// NewContext attempts to establish a context with the PCSC daemon. The returned +// context is only valid while the client is open. +func (c *Client) NewContext() (*Context, error) { + const scopeSystem = 0x0002 + req := establishRequest{ + Scope: scopeSystem, + RV: RVSuccess, + } + if err := c.sendMessage(commandEstablishContext, req, &req); err != nil { + return nil, fmt.Errorf("establish context: %v", err) + } + if req.RV != RVSuccess { + return nil, &RVError{RV: req.RV} + } + return &Context{client: c, context: req.Context}, nil +} + +// https://github.com/LudovicRousseau/PCSC/blob/1.9.0/src/winscard_msg.h#L118 +type releaseRequest struct { + Context uint32 + RV uint32 +} + +// Close releases the context with the PCSC daemon. +func (c *Context) Close() error { + req := releaseRequest{ + Context: c.context, + RV: RVSuccess, + } + if err := c.client.sendMessage(commandReleaseContext, req, &req); err != nil { + return fmt.Errorf("release context: %v", err) + } + if req.RV != RVSuccess { + return &RVError{RV: req.RV} + } + return nil +} + +// Connection represents a connection to a specific smartcard. +type Connection struct { + client *Client + context uint32 + card int32 +} + +// https://github.com/LudovicRousseau/PCSC/blob/1.9.0/src/winscard_msg.h#L141 +type connectRequest struct { + Context uint32 + Reader [maxReaderNameSize]byte + ShareMode uint32 + PreferredProtocols uint32 + Card int32 + ActiveProtocols uint32 + RV uint32 +} + +func (c *Context) Connect(reader string, mode uint32) (*Connection, error) { + req := connectRequest{ + Context: c.context, + ShareMode: Shared, + PreferredProtocols: ProtocolT1, + RV: RVSuccess, + } + + if len(reader)+1 > maxReaderNameSize { + return nil, fmt.Errorf("reader name too long") + } + copy(req.Reader[:], []byte(reader)) + + if err := c.client.sendMessage(commandConnect, req, &req); err != nil { + return nil, fmt.Errorf("send message: %v", err) + } + if req.RV != RVSuccess { + return nil, &RVError{RV: req.RV} + } + if req.Card == 0 { + return nil, fmt.Errorf("card returned no value") + } + return &Connection{ + client: c.client, context: c.context, card: req.Card, + }, nil +} + +//https://github.com/LudovicRousseau/PCSC/blob/1.9.0/src/winscard_msg.h#L172 +type disconnectRequest struct { + Card int32 + Disposition uint32 + RV uint32 +} + +func (c *Connection) Close() error { + req := disconnectRequest{ + Card: c.card, + Disposition: LeaveCard, + RV: RVSuccess, + } + + if err := c.client.sendMessage(commandDisconnect, req, &req); err != nil { + return fmt.Errorf("send message: %v", err) + } + if req.RV != RVSuccess { + return &RVError{RV: req.RV} + } + return nil +} + +// https://github.com/LudovicRousseau/PCSC/blob/1.9.0/src/winscard_msg.h#L184 +type beginRequest struct { + Card int32 + RV uint32 +} + +// BeginTransaction is called before transmitting data to the card. +func (c *Connection) BeginTransaction() error { + req := beginRequest{ + Card: c.card, + RV: RVSuccess, + } + + if err := c.client.sendMessage(commandBeginTransaction, req, &req); err != nil { + return fmt.Errorf("send message: %v", err) + } + if req.RV != RVSuccess { + return &RVError{RV: req.RV} + } + return nil +} + +// https://github.com/LudovicRousseau/PCSC/blob/1.9.0/src/winscard_msg.h#L195 +type endRequest struct { + Card int32 + Disposition uint32 + RV uint32 +} + +func (c *Connection) EndTransaction() error { + req := endRequest{ + Card: c.card, + Disposition: LeaveCard, + RV: RVSuccess, + } + + if err := c.client.sendMessage(commandEndTransaction, req, &req); err != nil { + return fmt.Errorf("send message: %v", err) + } + if req.RV != RVSuccess { + return &RVError{RV: req.RV} + } + return nil +} + +func (c *Connection) Transmit(b []byte) ([]byte, error) { + return nil, nil +} + func (c *Client) sendMessage(command uint32, req, resp interface{}) error { var data []byte if req != nil { @@ -156,16 +378,33 @@ func (c *Client) sendMessage(command uint32, req, resp interface{}) error { return fmt.Errorf("write request bytes: %v", err) } - if err := binary.Read(c.conn, nativeByteOrder, resp); err != nil { + respData := &bytes.Buffer{} + if err := binary.Read(io.TeeReader(c.conn, respData), nativeByteOrder, resp); err != nil { return fmt.Errorf("read response: %v", err) } + + fmt.Fprintln(os.Stderr, "Request:") + fmt.Fprintln(os.Stderr, hex.Dump(data[8:])) + + fmt.Fprintln(os.Stderr, "Response:") + fmt.Fprintln(os.Stderr, hex.Dump(respData.Bytes())) return nil } +// Config is used to modify client behavior. type Config struct { + // SocketPath can be used to override a path to the pcscd socket. This field + // is generally not required unless pcscd has been compiled with modified + // options. + // + // This value defaults to the pcsclite behavior, preferring the value of the + // PCSCLITE_CSOCK_NAME environment variable then defaulting to + // "/run/pcscd/pcscd.comm". SocketPath string } +// NewClient attempts to initialize a connection with pcscd. The context is used +// for dialing the unix domain socket. func NewClient(ctx context.Context, c *Config) (*Client, error) { p := c.SocketPath if p == "" { @@ -180,7 +419,10 @@ func NewClient(ctx context.Context, c *Config) (*Client, error) { if err != nil { return nil, fmt.Errorf("dial unix socket: %v", err) } - return &Client{ - conn: conn, - }, nil + client := &Client{conn: conn} + if err := client.checkVersion(); err != nil { + client.Close() + return nil, err + } + return client, nil } diff --git a/piv/internal/pcsc/pcsc_test.go b/piv/internal/pcsc/pcsc_test.go index 46d1501..5b266a1 100644 --- a/piv/internal/pcsc/pcsc_test.go +++ b/piv/internal/pcsc/pcsc_test.go @@ -26,11 +26,9 @@ func TestVersion(t *testing.T) { } defer c.Close() - major, minor, err := c.version() - if err != nil { - t.Fatalf("getting client version: %v", err) + if err := c.checkVersion(); err != nil { + t.Fatalf("checking client client version: %v", err) } - t.Log(major, minor) } func TestListReaders(t *testing.T) { @@ -40,9 +38,100 @@ func TestListReaders(t *testing.T) { } defer c.Close() - readers, err := c.readers() + if _, err := c.Readers(); err != nil { + t.Fatalf("listing readers: %v", err) + } +} + +func TestNewContext(t *testing.T) { + c, err := NewClient(context.Background(), &Config{}) + if err != nil { + t.Fatalf("create client: %v", err) + } + defer c.Close() + + ctx, err := c.NewContext() + if err != nil { + t.Fatalf("create context: %v", err) + } + if err := ctx.Close(); err != nil { + t.Fatalf("close context: %v", err) + } +} + +func TestConnect(t *testing.T) { + c, err := NewClient(context.Background(), &Config{}) + if err != nil { + t.Fatalf("create client: %v", err) + } + defer c.Close() + + ctx, err := c.NewContext() + if err != nil { + t.Fatalf("create context: %v", err) + } + defer func() { + if err := ctx.Close(); err != nil { + t.Fatalf("close context: %v", err) + } + }() + + readers, err := c.Readers() if err != nil { t.Fatalf("listing readers: %v", err) } - t.Log(readers) + if len(readers) == 0 { + t.Skipf("no readers available for test") + } + reader := readers[0] + + card, err := ctx.Connect(reader, Exclusive) + if err != nil { + t.Fatalf("new card: %v", err) + } + if err := card.Close(); err != nil { + t.Fatalf("close card: %v", err) + } +} + +func TestBeginTransaction(t *testing.T) { + c, err := NewClient(context.Background(), &Config{}) + if err != nil { + t.Fatalf("create client: %v", err) + } + defer c.Close() + + ctx, err := c.NewContext() + if err != nil { + t.Fatalf("create context: %v", err) + } + defer func() { + if err := ctx.Close(); err != nil { + t.Fatalf("close context: %v", err) + } + }() + + readers, err := c.Readers() + if err != nil { + t.Fatalf("listing readers: %v", err) + } + if len(readers) == 0 { + t.Skipf("no readers available for test") + } + reader := readers[0] + card, err := ctx.Connect(reader, Exclusive) + if err != nil { + t.Fatalf("new card: %v", err) + } + defer func() { + if err := card.Close(); err != nil { + t.Fatalf("close card: %v", err) + } + }() + if err := card.BeginTransaction(); err != nil { + t.Fatalf("begin transaction: %v", err) + } + if err := card.EndTransaction(); err != nil { + t.Fatalf("end transaction: %v", err) + } } diff --git a/piv/pcsc_linux.go b/piv/pcsc_linux.go index 6a5c078..18e052c 100644 --- a/piv/pcsc_linux.go +++ b/piv/pcsc_linux.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +// +build cgo,!pcscgo + package piv import "C" diff --git a/piv/pcsc_unix.go b/piv/pcsc_unix.go index 55d6318..fa8f5e7 100644 --- a/piv/pcsc_unix.go +++ b/piv/pcsc_unix.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// +build cgo,!pcscgo // +build darwin linux freebsd package piv diff --git a/piv/pcsc_unix_no_cgo.go b/piv/pcsc_unix_no_cgo.go new file mode 100644 index 0000000..4a717ba --- /dev/null +++ b/piv/pcsc_unix_no_cgo.go @@ -0,0 +1,91 @@ +// Copyright 2021 Google LLC +// +// 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. + +// +build !cgo pcscgo +// +build linux freebsd + +package piv + +import ( + "context" + "errors" + + "github.com/go-piv/piv-go/piv/internal/pcsc" +) + +func scCheck(err error) error { + var e *pcsc.RVError + if errors.As(err, &e) { + return &scErr{int64(e.RV)} + } + return err +} + +const rcSuccess = pcsc.RVSuccess + +type scContext struct { + client *pcsc.Client + ctx *pcsc.Context +} + +func newSCContext() (*scContext, error) { + c, err := pcsc.NewClient(context.Background(), &pcsc.Config{}) + if err != nil { + return nil, err + } + ctx, err := c.NewContext(pcsc.Exclusive) + if err != nil { + c.Close() + return nil, err + } + return &scContext{c, ctx}, nil +} + +func (c *scContext) Close() error { + err1 := c.ctx.Close() + if err := c.client.Close(); err != nil { + return err + } + return err1 +} + +func (c *scContext) ListReaders() ([]string, error) { + return c.client.Readers() +} + +type scHandle struct { +} + +func (c *scContext) Connect(reader string) (*scHandle, error) { + return nil, nil +} + +func (h *scHandle) Close() error { + return nil +} + +type scTx struct { +} + +func (h *scHandle) Begin() (*scTx, error) { + return nil, nil +} + +func (t *scTx) Close() error { + return nil +} + +func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) { + return false, nil, nil +}