Skip to content

Commit

Permalink
Add APDU debugging using a trace api similar to httptrace
Browse files Browse the repository at this point in the history
  • Loading branch information
Allen Reese committed Feb 19, 2024
1 parent 66ce787 commit f044341
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 2 deletions.
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
}
19 changes: 18 additions & 1 deletion piv/pcsc_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,23 +112,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 +154,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], respN, sw1, sw2)

Check failure on line 159 in piv/pcsc_unix.go

View workflow job for this annotation

GitHub Actions / Linux (1.19.x)

cannot use respN (variable of type _Ctype_ulong) as type int in argument to t.trace.TransmitResult
}

if sw1 == 0x90 && sw2 == 0x00 {
return false, resp[:respN-2], nil
}
Expand Down
19 changes: 18 additions & 1 deletion piv/pcsc_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ 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 {
Expand All @@ -169,12 +169,24 @@ func (t *scTx) Close() error {

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 +205,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], respN, sw1, sw2)
}

if sw1 == 0x90 && sw2 == 0x00 {
return false, resp[:respN-2], nil
}
Expand Down
25 changes: 25 additions & 0 deletions piv/piv.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package piv

import (
"bytes"
"context"
"crypto/des"
"crypto/rand"
"encoding/asn1"
Expand Down Expand Up @@ -113,6 +114,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,12 +128,24 @@ func (yk *YubiKey) Close() error {
return err1
}

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

// Open connects to a YubiKey smart card.
func Open(card string) (*YubiKey, error) {
var c client
return c.Open(card)
}

// OpenWithContext connects to a YubiKey smart card.
func OpenWithContext(ctx context.Context, card string) (*YubiKey, error) {
var c client
return c.OpenWithContext(ctx, card)
}

// client is a smart card client and may be exported in the future to allow
// configuration for the top level Open() and Cards() APIs.
type client struct {
Expand All @@ -150,6 +165,10 @@ func (c *client) Cards() ([]string, error) {
}

func (c *client) Open(card string) (*YubiKey, error) {
return c.OpenWithContext(context.Background(), card)
}

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

trace := ContextClientTrace(context)
if trace != nil {
tx.WithClientTrace(trace)
}

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

0 comments on commit f044341

Please # to comment.