Skip to content

Commit f044341

Browse files
author
Allen Reese
committed
Add APDU debugging using a trace api similar to httptrace
1 parent 66ce787 commit f044341

File tree

4 files changed

+169
-2
lines changed

4 files changed

+169
-2
lines changed

piv/pcsc_trace.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License");
2+
// you may not use this file except in compliance with the License.
3+
// You may obtain a copy of the License at
4+
//
5+
// https://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
package piv
13+
14+
import (
15+
"context"
16+
"reflect"
17+
)
18+
19+
// ClientTrace is a set of hooks to run at various stages pcsc calls.
20+
// Any particular hook may be nil. Functions may be
21+
// called concurrently from different goroutines and some may be called
22+
// after the request has completed or failed.
23+
//
24+
// ClientTrace is adapted from httptrace.ClientTrace.
25+
// ClientTrace currently traces a single pcsc call, providing the apdus
26+
// that were sent.
27+
type ClientTrace struct {
28+
// Transmit is called before an APDU is transmitted to the card.
29+
// The byte array is the complete contents of the request being sent to
30+
// SCardTransmit.
31+
// Transmit is called from scTx.
32+
Transmit func(req []byte)
33+
34+
// TransmitResult is called afterr an APDU is transmitted to the card.
35+
// req is the contents of the request.
36+
// resp is the contents of the response.
37+
// respN is the number of bytes returned in the response.
38+
// s1,sw2 are the last 2 bytes of the response.
39+
// sw1,sw2 are the contents of last 2 bytes of the response.
40+
// an apduErr contains sw1,sw2.
41+
// if sw1==0x61, there is more data.
42+
// TransmitResult is called from scTx.
43+
TransmitResult func(req, resp []byte, respN int, sw1, sw2 byte)
44+
}
45+
46+
// unique type to prevent assignment.
47+
type clientEventContextKey struct{}
48+
49+
// ContextClientTrace returns the [ClientTrace] associated with the
50+
// provided context. If none, it returns nil.
51+
func ContextClientTrace(ctx context.Context) *ClientTrace {
52+
trace, _ := ctx.Value(clientEventContextKey{}).(*ClientTrace)
53+
return trace
54+
}
55+
56+
// compose modifies t such that it respects the previously-registered hooks in old,
57+
// subject to the composition policy requested in t.Compose.
58+
func (t *ClientTrace) compose(old *ClientTrace) {
59+
if old == nil {
60+
return
61+
}
62+
tv := reflect.ValueOf(t).Elem()
63+
ov := reflect.ValueOf(old).Elem()
64+
structType := tv.Type()
65+
for i := 0; i < structType.NumField(); i++ {
66+
tf := tv.Field(i)
67+
hookType := tf.Type()
68+
if hookType.Kind() != reflect.Func {
69+
continue
70+
}
71+
of := ov.Field(i)
72+
if of.IsNil() {
73+
continue
74+
}
75+
if tf.IsNil() {
76+
tf.Set(of)
77+
continue
78+
}
79+
80+
// Make a copy of tf for tf to call. (Otherwise it
81+
// creates a recursive call cycle and stack overflows)
82+
tfCopy := reflect.ValueOf(tf.Interface())
83+
84+
// We need to call both tf and of in some order.
85+
newFunc := reflect.MakeFunc(hookType, func(args []reflect.Value) []reflect.Value {
86+
tfCopy.Call(args)
87+
return of.Call(args)
88+
})
89+
tv.Field(i).Set(newFunc)
90+
}
91+
}
92+
93+
// WithClientTrace returns a new context based on the provided parent
94+
// ctx. HTTP client requests made with the returned context will use
95+
// the provided trace hooks, in addition to any previous hooks
96+
// registered with ctx. Any hooks defined in the provided trace will
97+
// be called first.
98+
func WithClientTrace(ctx context.Context, trace *ClientTrace) context.Context {
99+
if trace == nil {
100+
panic("nil trace")
101+
}
102+
old := ContextClientTrace(ctx)
103+
trace.compose(old)
104+
105+
ctx = context.WithValue(ctx, clientEventContextKey{}, trace)
106+
107+
return ctx
108+
}

piv/pcsc_unix.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,23 +112,35 @@ func (h *scHandle) Close() error {
112112

113113
type scTx struct {
114114
h C.SCARDHANDLE
115+
// If trace is not nil, then trace.Transmit and trace.TransmitResult will be called.
116+
trace *ClientTrace
115117
}
116118

117119
func (h *scHandle) Begin() (*scTx, error) {
118120
if err := scCheck(C.SCardBeginTransaction(h.h)); err != nil {
119121
return nil, err
120122
}
121-
return &scTx{h.h}, nil
123+
return &scTx{h.h, nil}, nil
122124
}
123125

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

130+
// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
131+
func (t *scTx) WithClientTrace(clientTrace *ClientTrace) {
132+
t.trace = clientTrace
133+
}
134+
128135
func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) {
129136
var resp [C.MAX_BUFFER_SIZE_EXTENDED]byte
130137
reqN := C.DWORD(len(req))
131138
respN := C.DWORD(len(resp))
139+
140+
if t.trace != nil && t.trace.Transmit != nil {
141+
t.trace.Transmit(req[:])
142+
}
143+
132144
rc := C.SCardTransmit(
133145
t.h,
134146
C.SCARD_PCI_T1,
@@ -142,6 +154,11 @@ func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) {
142154
}
143155
sw1 := resp[respN-2]
144156
sw2 := resp[respN-1]
157+
158+
if t.trace != nil && t.trace.TransmitResult != nil {
159+
t.trace.TransmitResult(req[:], resp[:respN], respN, sw1, sw2)
160+
}
161+
145162
if sw1 == 0x90 && sw2 == 0x00 {
146163
return false, resp[:respN-2], nil
147164
}

piv/pcsc_windows.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func (h *scHandle) Begin() (*scTx, error) {
159159
if err := scCheck(r0); err != nil {
160160
return nil, err
161161
}
162-
return &scTx{h.handle}, nil
162+
return &scTx{h.handle, nil}, nil
163163
}
164164

165165
func (t *scTx) Close() error {
@@ -169,12 +169,24 @@ func (t *scTx) Close() error {
169169

170170
type scTx struct {
171171
handle syscall.Handle
172+
// If trace is not nil, then trace.Transmit and trace.TransmitResult will be called.
173+
trace *ClientTrace
174+
}
175+
176+
// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
177+
func (t *scTx) WithClientTrace(clientTrace *ClientTrace) {
178+
t.trace = clientTrace
172179
}
173180

174181
func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) {
175182
var resp [maxBufferSizeExtended]byte
176183
reqN := len(req)
177184
respN := len(resp)
185+
186+
if t.trace != nil && t.trace.Transmit != nil {
187+
t.trace.Transmit(req[:])
188+
}
189+
178190
r0, _, _ := procSCardTransmit.Call(
179191
uintptr(t.handle),
180192
uintptr(scardPCIT1),
@@ -193,6 +205,11 @@ func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) {
193205
}
194206
sw1 := resp[respN-2]
195207
sw2 := resp[respN-1]
208+
209+
if t.trace != nil && t.trace.TransmitResult != nil {
210+
t.trace.TransmitResult(req[:], resp[:respN], respN, sw1, sw2)
211+
}
212+
196213
if sw1 == 0x90 && sw2 == 0x00 {
197214
return false, resp[:respN-2], nil
198215
}

piv/piv.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package piv
1616

1717
import (
1818
"bytes"
19+
"context"
1920
"crypto/des"
2021
"crypto/rand"
2122
"encoding/asn1"
@@ -113,6 +114,8 @@ type YubiKey struct {
113114
// YubiKey's version or PIV version? A NEO reports v1.0.4. Figure this out
114115
// before exposing an API.
115116
version *version
117+
118+
trace *ClientTrace
116119
}
117120

118121
// Close releases the connection to the smart card.
@@ -125,12 +128,24 @@ func (yk *YubiKey) Close() error {
125128
return err1
126129
}
127130

131+
// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
132+
func (yk *YubiKey) WithClientTrace(clientTrace *ClientTrace) {
133+
yk.trace = clientTrace
134+
yk.tx.WithClientTrace(clientTrace)
135+
}
136+
128137
// Open connects to a YubiKey smart card.
129138
func Open(card string) (*YubiKey, error) {
130139
var c client
131140
return c.Open(card)
132141
}
133142

143+
// OpenWithContext connects to a YubiKey smart card.
144+
func OpenWithContext(ctx context.Context, card string) (*YubiKey, error) {
145+
var c client
146+
return c.OpenWithContext(ctx, card)
147+
}
148+
134149
// client is a smart card client and may be exported in the future to allow
135150
// configuration for the top level Open() and Cards() APIs.
136151
type client struct {
@@ -150,6 +165,10 @@ func (c *client) Cards() ([]string, error) {
150165
}
151166

152167
func (c *client) Open(card string) (*YubiKey, error) {
168+
return c.OpenWithContext(context.Background(), card)
169+
}
170+
171+
func (c *client) OpenWithContext(context context.Context, card string) (*YubiKey, error) {
153172
ctx, err := newSCContext()
154173
if err != nil {
155174
return nil, fmt.Errorf("connecting to smart card daemon: %w", err)
@@ -164,6 +183,12 @@ func (c *client) Open(card string) (*YubiKey, error) {
164183
if err != nil {
165184
return nil, fmt.Errorf("beginning smart card transaction: %w", err)
166185
}
186+
187+
trace := ContextClientTrace(context)
188+
if trace != nil {
189+
tx.WithClientTrace(trace)
190+
}
191+
167192
if err := ykSelectApplication(tx, aidPIV[:]); err != nil {
168193
tx.Close()
169194
return nil, fmt.Errorf("selecting piv applet: %w", err)

0 commit comments

Comments
 (0)