Skip to content

Commit d480045

Browse files
committed
Add APDU debugging using a trace api similar to httptrace
A ClientTrace struct can be set on a YubiKey object, or a piv.Client struct can be created and Open called against that.
1 parent 5a76b44 commit d480045

File tree

5 files changed

+207
-16
lines changed

5 files changed

+207
-16
lines changed

piv/pcsc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
)
2222

2323
func runContextTest(t *testing.T, f func(t *testing.T, c *scContext)) {
24-
ctx, err := newSCContext()
24+
ctx, err := newSCContext(nil)
2525
if err != nil {
2626
t.Fatalf("creating context: %v", err)
2727
}

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: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,17 @@ import (
4242
const rcSuccess = C.SCARD_S_SUCCESS
4343

4444
type scContext struct {
45-
ctx C.SCARDCONTEXT
45+
ctx C.SCARDCONTEXT
46+
trace *ClientTrace
4647
}
4748

48-
func newSCContext() (*scContext, error) {
49+
func newSCContext(trace *ClientTrace) (*scContext, error) {
4950
var ctx C.SCARDCONTEXT
5051
rc := C.SCardEstablishContext(C.SCARD_SCOPE_SYSTEM, nil, nil, &ctx)
5152
if err := scCheck(rc); err != nil {
5253
return nil, err
5354
}
54-
return &scContext{ctx: ctx}, nil
55+
return &scContext{ctx: ctx, trace: trace}, nil
5556
}
5657

5758
func (c *scContext) Close() error {
@@ -88,8 +89,14 @@ func (c *scContext) ListReaders() ([]string, error) {
8889
return readers, nil
8990
}
9091

92+
// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
93+
func (c *scContext) WithClientTrace(clientTrace *ClientTrace) {
94+
c.trace = clientTrace
95+
}
96+
9197
type scHandle struct {
92-
h C.SCARDHANDLE
98+
h C.SCARDHANDLE
99+
trace *ClientTrace
93100
}
94101

95102
func (c *scContext) Connect(reader string) (*scHandle, error) {
@@ -103,7 +110,12 @@ func (c *scContext) Connect(reader string) (*scHandle, error) {
103110
if err := scCheck(rc); err != nil {
104111
return nil, err
105112
}
106-
return &scHandle{handle}, nil
113+
return &scHandle{h: handle, trace: c.trace}, nil
114+
}
115+
116+
// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
117+
func (h *scHandle) WithClientTrace(clientTrace *ClientTrace) {
118+
h.trace = clientTrace
107119
}
108120

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

113125
type scTx struct {
114126
h C.SCARDHANDLE
127+
// If trace is not nil, then trace.Transmit and trace.TransmitResult will be called.
128+
trace *ClientTrace
115129
}
116130

117131
func (h *scHandle) Begin() (*scTx, error) {
118132
if err := scCheck(C.SCardBeginTransaction(h.h)); err != nil {
119133
return nil, err
120134
}
121-
return &scTx{h.h}, nil
135+
return &scTx{h.h, nil}, nil
122136
}
123137

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

142+
// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
143+
func (t *scTx) WithClientTrace(clientTrace *ClientTrace) {
144+
t.trace = clientTrace
145+
}
146+
128147
func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) {
129148
var resp [C.MAX_BUFFER_SIZE_EXTENDED]byte
130149
reqN := C.DWORD(len(req))
131150
respN := C.DWORD(len(resp))
151+
152+
if t.trace != nil && t.trace.Transmit != nil {
153+
t.trace.Transmit(req[:])
154+
}
155+
132156
rc := C.SCardTransmit(
133157
t.h,
134158
C.SCARD_PCI_T1,
@@ -142,6 +166,11 @@ func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) {
142166
}
143167
sw1 := resp[respN-2]
144168
sw2 := resp[respN-1]
169+
170+
if t.trace != nil && t.trace.TransmitResult != nil {
171+
t.trace.TransmitResult(req[:], resp[:respN], int(respN), sw1, sw2)
172+
}
173+
145174
if sw1 == 0x90 && sw2 == 0x00 {
146175
return false, resp[:respN-2], nil
147176
}

piv/pcsc_windows.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,11 @@ func isRCNoReaders(rc uintptr) bool {
5454
}
5555

5656
type scContext struct {
57-
ctx syscall.Handle
57+
ctx syscall.Handle
58+
trace *ClientTrace
5859
}
5960

60-
func newSCContext() (*scContext, error) {
61+
func newSCContext(trace *ClientTrace) (*scContext, error) {
6162
var ctx syscall.Handle
6263

6364
r0, _, _ := procSCardEstablishContext.Call(
@@ -69,7 +70,7 @@ func newSCContext() (*scContext, error) {
6970
if err := scCheck(r0); err != nil {
7071
return nil, err
7172
}
72-
return &scContext{ctx: ctx}, nil
73+
return &scContext{ctx: ctx, trace: trace}, nil
7374
}
7475

7576
func (c *scContext) Close() error {
@@ -142,11 +143,17 @@ func (c *scContext) Connect(reader string) (*scHandle, error) {
142143
if err := scCheck(r0); err != nil {
143144
return nil, err
144145
}
145-
return &scHandle{handle}, nil
146+
return &scHandle{handle: handle, trace: c.trace}, nil
147+
}
148+
149+
// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
150+
func (c *scContext) WithClientTrace(clientTrace *ClientTrace) {
151+
c.trace = clientTrace
146152
}
147153

148154
type scHandle struct {
149155
handle syscall.Handle
156+
trace *ClientTrace
150157
}
151158

152159
func (h *scHandle) Close() error {
@@ -159,22 +166,39 @@ func (h *scHandle) Begin() (*scTx, error) {
159166
if err := scCheck(r0); err != nil {
160167
return nil, err
161168
}
162-
return &scTx{h.handle}, nil
169+
return &scTx{h.handle, nil}, nil
163170
}
164171

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

177+
// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
178+
func (h *scHandle) WithClientTrace(clientTrace *ClientTrace) {
179+
h.trace = clientTrace
180+
}
181+
170182
type scTx struct {
171183
handle syscall.Handle
184+
// If trace is not nil, then trace.Transmit and trace.TransmitResult will be called.
185+
trace *ClientTrace
186+
}
187+
188+
// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
189+
func (t *scTx) WithClientTrace(clientTrace *ClientTrace) {
190+
t.trace = clientTrace
172191
}
173192

174193
func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) {
175194
var resp [maxBufferSizeExtended]byte
176195
reqN := len(req)
177196
respN := len(resp)
197+
198+
if t.trace != nil && t.trace.Transmit != nil {
199+
t.trace.Transmit(req[:])
200+
}
201+
178202
r0, _, _ := procSCardTransmit.Call(
179203
uintptr(t.handle),
180204
uintptr(scardPCIT1),
@@ -193,6 +217,11 @@ func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) {
193217
}
194218
sw1 := resp[respN-2]
195219
sw2 := resp[respN-1]
220+
221+
if t.trace != nil && t.trace.TransmitResult != nil {
222+
t.trace.TransmitResult(req[:], resp[:respN], int(respN), sw1, sw2)
223+
}
224+
196225
if sw1 == 0x90 && sw2 == 0x00 {
197226
return false, resp[:respN-2], nil
198227
}

piv/piv.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ type YubiKey struct {
113113
// YubiKey's version or PIV version? A NEO reports v1.0.4. Figure this out
114114
// before exposing an API.
115115
version *version
116+
117+
trace *ClientTrace
116118
}
117119

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

128-
// Open connects to a YubiKey smart card.
130+
// WithClientTrace can be passed an instance of ClientTrace to trace the apdu's sent.
131+
func (yk *YubiKey) WithClientTrace(clientTrace *ClientTrace) {
132+
yk.trace = clientTrace
133+
yk.ctx.WithClientTrace(clientTrace)
134+
yk.h.WithClientTrace(clientTrace)
135+
yk.tx.WithClientTrace(clientTrace)
136+
}
137+
138+
// Client allows a yubikey to be opened with tracing.
139+
type Client struct {
140+
Trace *ClientTrace
141+
}
142+
143+
func (p *Client) Open(card string) (*YubiKey, error) {
144+
c := client{
145+
Rand: nil,
146+
trace: p.Trace,
147+
}
148+
149+
return c.Open(card)
150+
}
151+
129152
func Open(card string) (*YubiKey, error) {
130153
var c client
131154
return c.Open(card)
@@ -137,11 +160,12 @@ type client struct {
137160
// Rand is a cryptographic source of randomness used for card challenges.
138161
//
139162
// If nil, defaults to crypto.Rand.
140-
Rand io.Reader
163+
Rand io.Reader
164+
trace *ClientTrace
141165
}
142166

143167
func (c *client) Cards() ([]string, error) {
144-
ctx, err := newSCContext()
168+
ctx, err := newSCContext(c.trace)
145169
if err != nil {
146170
return nil, fmt.Errorf("connecting to pcsc: %w", err)
147171
}
@@ -150,7 +174,7 @@ func (c *client) Cards() ([]string, error) {
150174
}
151175

152176
func (c *client) Open(card string) (*YubiKey, error) {
153-
ctx, err := newSCContext()
177+
ctx, err := newSCContext(c.trace)
154178
if err != nil {
155179
return nil, fmt.Errorf("connecting to smart card daemon: %w", err)
156180
}
@@ -164,6 +188,7 @@ func (c *client) Open(card string) (*YubiKey, error) {
164188
if err != nil {
165189
return nil, fmt.Errorf("beginning smart card transaction: %w", err)
166190
}
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)