Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add APDU debugging using a trace api similar to httptrace #138

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion piv/pcsc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
)

func runContextTest(t *testing.T, f func(t *testing.T, c *scContext)) {
ctx, err := newSCContext()
ctx, err := newSCContext(nil)
if err != nil {
t.Fatalf("creating context: %v", err)
}
Expand Down
108 changes: 108 additions & 0 deletions piv/pcsc_trace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// 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.
package piv

import (
"context"
"reflect"
)

// ClientTrace is a set of hooks to run at various stages pcsc calls.
// Any particular hook may be nil. Functions may be
// called concurrently from different goroutines and some may be called
// after the request has completed or failed.
//
// ClientTrace is adapted from httptrace.ClientTrace.
// ClientTrace currently traces a single pcsc call, providing the apdus
// that were sent.
type ClientTrace struct {
// Transmit is called before an APDU is transmitted to the card.
// The byte array is the complete contents of the request being sent to
// SCardTransmit.
// Transmit is called from scTx.
Transmit func(req []byte)

// TransmitResult is called afterr an APDU is transmitted to the card.
// req is the contents of the request.
// resp is the contents of the response.
// respN is the number of bytes returned in the response.
// s1,sw2 are the last 2 bytes of the response.
// sw1,sw2 are the contents of last 2 bytes of the response.
// an apduErr contains sw1,sw2.
// if sw1==0x61, there is more data.
// TransmitResult is called from scTx.
TransmitResult func(req, resp []byte, respN int, sw1, sw2 byte)
}

// unique type to prevent assignment.
type clientEventContextKey struct{}

// ContextClientTrace returns the [ClientTrace] associated with the
// provided context. If none, it returns nil.
func ContextClientTrace(ctx context.Context) *ClientTrace {
trace, _ := ctx.Value(clientEventContextKey{}).(*ClientTrace)
return trace
}

// compose modifies t such that it respects the previously-registered hooks in old,
// subject to the composition policy requested in t.Compose.
func (t *ClientTrace) compose(old *ClientTrace) {
if old == nil {
return
}
tv := reflect.ValueOf(t).Elem()
ov := reflect.ValueOf(old).Elem()
structType := tv.Type()
for i := 0; i < structType.NumField(); i++ {
tf := tv.Field(i)
hookType := tf.Type()
if hookType.Kind() != reflect.Func {
continue
}
of := ov.Field(i)
if of.IsNil() {
continue
}
if tf.IsNil() {
tf.Set(of)
continue
}

// Make a copy of tf for tf to call. (Otherwise it
// creates a recursive call cycle and stack overflows)
tfCopy := reflect.ValueOf(tf.Interface())

// We need to call both tf and of in some order.
newFunc := reflect.MakeFunc(hookType, func(args []reflect.Value) []reflect.Value {
tfCopy.Call(args)
return of.Call(args)
})
tv.Field(i).Set(newFunc)
}
}

// WithClientTrace returns a new context based on the provided parent
// ctx. HTTP client requests made with the returned context will use
// the provided trace hooks, in addition to any previous hooks
// registered with ctx. Any hooks defined in the provided trace will
// be called first.
func WithClientTrace(ctx context.Context, trace *ClientTrace) context.Context {
if trace == nil {
panic("nil trace")
}
old := ContextClientTrace(ctx)
trace.compose(old)

ctx = context.WithValue(ctx, clientEventContextKey{}, trace)

return ctx
}
41 changes: 35 additions & 6 deletions piv/pcsc_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,17 @@ import (
const rcSuccess = C.SCARD_S_SUCCESS

type scContext struct {
ctx C.SCARDCONTEXT
ctx C.SCARDCONTEXT
trace *ClientTrace
}

func newSCContext() (*scContext, error) {
func newSCContext(trace *ClientTrace) (*scContext, error) {
var ctx C.SCARDCONTEXT
rc := C.SCardEstablishContext(C.SCARD_SCOPE_SYSTEM, nil, nil, &ctx)
if err := scCheck(rc); err != nil {
return nil, err
}
return &scContext{ctx: ctx}, nil
return &scContext{ctx: ctx, trace: trace}, nil
}

func (c *scContext) Close() error {
Expand Down Expand Up @@ -88,8 +89,14 @@ func (c *scContext) ListReaders() ([]string, error) {
return readers, nil
}

// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
func (c *scContext) WithClientTrace(clientTrace *ClientTrace) {
c.trace = clientTrace
}

type scHandle struct {
h C.SCARDHANDLE
h C.SCARDHANDLE
trace *ClientTrace
}

func (c *scContext) Connect(reader string) (*scHandle, error) {
Expand All @@ -103,7 +110,12 @@ func (c *scContext) Connect(reader string) (*scHandle, error) {
if err := scCheck(rc); err != nil {
return nil, err
}
return &scHandle{handle}, nil
return &scHandle{h: handle, trace: c.trace}, nil
}

// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
func (h *scHandle) WithClientTrace(clientTrace *ClientTrace) {
h.trace = clientTrace
}

func (h *scHandle) Close() error {
Expand All @@ -112,23 +124,35 @@ func (h *scHandle) Close() error {

type scTx struct {
h C.SCARDHANDLE
// If trace is not nil, then trace.Transmit and trace.TransmitResult will be called.
trace *ClientTrace
}

func (h *scHandle) Begin() (*scTx, error) {
if err := scCheck(C.SCardBeginTransaction(h.h)); err != nil {
return nil, err
}
return &scTx{h.h}, nil
return &scTx{h.h, nil}, nil
}

func (t *scTx) Close() error {
return scCheck(C.SCardEndTransaction(t.h, C.SCARD_LEAVE_CARD))
}

// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
func (t *scTx) WithClientTrace(clientTrace *ClientTrace) {
t.trace = clientTrace
}

func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) {
var resp [C.MAX_BUFFER_SIZE_EXTENDED]byte
reqN := C.DWORD(len(req))
respN := C.DWORD(len(resp))

if t.trace != nil && t.trace.Transmit != nil {
t.trace.Transmit(req[:])
}

rc := C.SCardTransmit(
t.h,
C.SCARD_PCI_T1,
Expand All @@ -142,6 +166,11 @@ func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) {
}
sw1 := resp[respN-2]
sw2 := resp[respN-1]

if t.trace != nil && t.trace.TransmitResult != nil {
t.trace.TransmitResult(req[:], resp[:respN], int(respN), sw1, sw2)
}

if sw1 == 0x90 && sw2 == 0x00 {
return false, resp[:respN-2], nil
}
Expand Down
39 changes: 34 additions & 5 deletions piv/pcsc_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ func isRCNoReaders(rc uintptr) bool {
}

type scContext struct {
ctx syscall.Handle
ctx syscall.Handle
trace *ClientTrace
}

func newSCContext() (*scContext, error) {
func newSCContext(trace *ClientTrace) (*scContext, error) {
var ctx syscall.Handle

r0, _, _ := procSCardEstablishContext.Call(
Expand All @@ -69,7 +70,7 @@ func newSCContext() (*scContext, error) {
if err := scCheck(r0); err != nil {
return nil, err
}
return &scContext{ctx: ctx}, nil
return &scContext{ctx: ctx, trace: trace}, nil
}

func (c *scContext) Close() error {
Expand Down Expand Up @@ -142,11 +143,17 @@ func (c *scContext) Connect(reader string) (*scHandle, error) {
if err := scCheck(r0); err != nil {
return nil, err
}
return &scHandle{handle}, nil
return &scHandle{handle: handle, trace: c.trace}, nil
}

// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
func (c *scContext) WithClientTrace(clientTrace *ClientTrace) {
c.trace = clientTrace
}

type scHandle struct {
handle syscall.Handle
trace *ClientTrace
}

func (h *scHandle) Close() error {
Expand All @@ -159,22 +166,39 @@ func (h *scHandle) Begin() (*scTx, error) {
if err := scCheck(r0); err != nil {
return nil, err
}
return &scTx{h.handle}, nil
return &scTx{h.handle, nil}, nil
}

func (t *scTx) Close() error {
r0, _, _ := procSCardEndTransaction.Call(uintptr(t.handle), scardLeaveCard)
return scCheck(r0)
}

// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
func (h *scHandle) WithClientTrace(clientTrace *ClientTrace) {
h.trace = clientTrace
}

type scTx struct {
handle syscall.Handle
// If trace is not nil, then trace.Transmit and trace.TransmitResult will be called.
trace *ClientTrace
}

// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
func (t *scTx) WithClientTrace(clientTrace *ClientTrace) {
t.trace = clientTrace
}

func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) {
var resp [maxBufferSizeExtended]byte
reqN := len(req)
respN := len(resp)

if t.trace != nil && t.trace.Transmit != nil {
t.trace.Transmit(req[:])
}

r0, _, _ := procSCardTransmit.Call(
uintptr(t.handle),
uintptr(scardPCIT1),
Expand All @@ -193,6 +217,11 @@ func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) {
}
sw1 := resp[respN-2]
sw2 := resp[respN-1]

if t.trace != nil && t.trace.TransmitResult != nil {
t.trace.TransmitResult(req[:], resp[:respN], int(respN), sw1, sw2)
}

if sw1 == 0x90 && sw2 == 0x00 {
return false, resp[:respN-2], nil
}
Expand Down
33 changes: 29 additions & 4 deletions piv/piv.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ type YubiKey struct {
// YubiKey's version or PIV version? A NEO reports v1.0.4. Figure this out
// before exposing an API.
version *version

trace *ClientTrace
}

// Close releases the connection to the smart card.
Expand All @@ -125,7 +127,28 @@ func (yk *YubiKey) Close() error {
return err1
}

// Open connects to a YubiKey smart card.
// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
func (yk *YubiKey) WithClientTrace(clientTrace *ClientTrace) {
yk.trace = clientTrace
yk.ctx.WithClientTrace(clientTrace)
yk.h.WithClientTrace(clientTrace)
yk.tx.WithClientTrace(clientTrace)
}

// Client allows a yubikey to be opened with tracing.
type Client struct {
Trace *ClientTrace
}

func (p *Client) Open(card string) (*YubiKey, error) {
c := client{
Rand: nil,
trace: p.Trace,
}

return c.Open(card)
}

func Open(card string) (*YubiKey, error) {
var c client
return c.Open(card)
Expand All @@ -137,11 +160,12 @@ type client struct {
// Rand is a cryptographic source of randomness used for card challenges.
//
// If nil, defaults to crypto.Rand.
Rand io.Reader
Rand io.Reader
trace *ClientTrace
}

func (c *client) Cards() ([]string, error) {
ctx, err := newSCContext()
ctx, err := newSCContext(c.trace)
if err != nil {
return nil, fmt.Errorf("connecting to pcsc: %w", err)
}
Expand All @@ -150,7 +174,7 @@ func (c *client) Cards() ([]string, error) {
}

func (c *client) Open(card string) (*YubiKey, error) {
ctx, err := newSCContext()
ctx, err := newSCContext(c.trace)
if err != nil {
return nil, fmt.Errorf("connecting to smart card daemon: %w", err)
}
Expand All @@ -164,6 +188,7 @@ func (c *client) Open(card string) (*YubiKey, error) {
if err != nil {
return nil, fmt.Errorf("beginning smart card transaction: %w", err)
}

if err := ykSelectApplication(tx, aidPIV[:]); err != nil {
tx.Close()
return nil, fmt.Errorf("selecting piv applet: %w", err)
Expand Down
Loading