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

Implement Read/Write CHUID #67

Open
wants to merge 5 commits 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_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func (c *scContext) Connect(reader string) (*scHandle, error) {
activeProtocol C.DWORD
)
rc := C.SCardConnect(c.ctx, C.CString(reader),
C.SCARD_SHARE_EXCLUSIVE, C.SCARD_PROTOCOL_T1,
C.SCARD_SHARE_SHARED, C.SCARD_PROTOCOL_T1,
&handle, &activeProtocol)
if err := scCheck(rc); err != nil {
return nil, err
Expand Down
139 changes: 139 additions & 0 deletions piv/piv.go
Original file line number Diff line number Diff line change
Expand Up @@ -846,3 +846,142 @@ func ykSetProtectedMetadata(tx *scTx, key [24]byte, m *Metadata) error {
}
return nil
}

// CardID is the Card Holder Unique Identifier with settable GUID
// Raw contains the whole object
// GUID contains the Card Universally Unique Identifier.
type CardID struct {
raw []byte
GUID [16]byte
}

// CardID returns the card CHUID with the GUID extracted
// If the CHUID is not set, the returned error wraps ErrNotFound.
func (yk *YubiKey) CardID() (*CardID, error) {
return ykGetCardID(yk.tx)
}

/*
* From https://github.com/Yubico/yubico-piv-tool/blob/ebee7f63b85fe4373efc4d8d44cbe5fe321c158c/lib/util.c#L44
* Format defined in SP-800-73-4, Appendix A, Table 9
*
* FASC-N containing S9999F9999F999999F0F1F0000000000300001E encoded in
* 4-bit BCD with 1 bit parity. run through the tools/fasc.pl script to get
* bytes. This CHUID has an expiry of 2030-01-01.
*
* Defined fields:
* - 0x30: FASC-N (hard-coded)
* - 0x34: Card UUID / GUID (settable)
* - 0x35: Exp. Date (hard-coded)
* - 0x3e: Signature (hard-coded, empty)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these actually not settable or is that just the API that yubico's tools expose?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They can be set - it's hardcoded in the yubico implementation, i was thinking of eventually add these and implement the BCD encoding too in a future PR.

* - 0xfe: Error Detection Code (hard-coded)
*/

var chuidTemplate = []byte{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this come from? Would default field values be better?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is coming straight from yubico tools. they generated that - for now, it would suggest to do the same, and later add the fields in the CardID struct and add the encoding.

0x30, 0x19, 0xd4, 0xe7, 0x39, 0xda, 0x73, 0x9c, 0xed, 0x39, 0xce, 0x73, 0x9d,
0x83, 0x68, 0x58, 0x21, 0x08, 0x42, 0x10, 0x84, 0x21, 0xc8, 0x42, 0x10, 0xc3,
0xeb, 0x34, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x08, 0x32, 0x30, 0x33, 0x30, 0x30,
0x31, 0x30, 0x31, 0x3e, 0x00, 0xfe, 0x00,
}

const uuidOffset = 29

func ykGetCardID(tx *scTx) (*CardID, error) {
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=17
// OID for CardId is 5FC102

cmd := apdu{
instruction: insGetData,
param1: 0x3f,
param2: 0xff,
data: []byte{
0x5c, // Tag list
0x03,
0x5f,
0xc1,
0x02,
},
}
resp, err := tx.Transmit(cmd)
if err != nil {
return nil, fmt.Errorf("command failed: %w", err)
}
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=85
obj, _, err := unmarshalASN1(resp, 1, 0x13) // tag 0x53
if err != nil {
return nil, fmt.Errorf("unmarshaling response: %v", err)
}

var (
id CardID
guid []byte
v asn1.RawValue
)
id.raw = obj
d := obj
// if the GUID is present assign it
for len(d) > 0 {
rest, err := asn1.Unmarshal(d, &v)
if err != nil {
return nil, fmt.Errorf("unmarshaling CHUID: %v", err)
}
// GUID is tag 34 / len of 16
if bytes.HasPrefix(v.FullBytes, []byte{0x34}) {
if len(v.Bytes) != 16 {
return nil, fmt.Errorf("incorrect guid length")
}
guid = v.Bytes
break
}
d = rest
}
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=19
// The Global Unique Identification number (GUID) field must be present, and shall include a
// Card Universally Unique Identifier (UUID)
if len(guid) == 0 {
return nil, fmt.Errorf("missing guid")
}

copy(id.GUID[:], guid)

return &id, nil
}

// SetCardID initialize the CHUID card object using a predefined template
func (yk *YubiKey) SetCardID(key [24]byte, id *CardID) error {
return ykSetCardID(yk.tx, key, id)
}

func ykSetCardID(tx *scTx, key [24]byte, id *CardID) error {

id.raw = make([]byte, len(chuidTemplate))
copy(id.raw, chuidTemplate)
copy(id.raw[uuidOffset:], id.GUID[:])

data := append([]byte{
0x5c, // Tag list
0x03,
0x5f,
0xc1,
0x02,
}, marshalASN1(0x53, id.raw)...)

cmd := apdu{
instruction: insPutData,
param1: 0x3f,
param2: 0xff,
data: data,
}

if err := ykAuthenticate(tx, key, rand.Reader); err != nil {
return fmt.Errorf("authenticating with key: %w", err)
}

_, err := tx.Transmit(cmd)
if err != nil {
return fmt.Errorf("command failed: %w", err)
}

return nil
}
27 changes: 27 additions & 0 deletions piv/piv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,3 +471,30 @@ func TestMetadataAdditoinalFields(t *testing.T) {
t.Errorf("(*Metadata.marshal, got=0x%x, want=0x%x", got, want)
}
}

func TestYubiKeyCardId(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
yk, close := newTestYubiKey(t)
defer close()
if err := yk.Reset(); err != nil {
t.Fatalf("resetting yubikey: %v", err)
}
if _, err := yk.CardID(); !errors.Is(err, ErrNotFound) {
t.Fatalf("expecting not found chuid")
}
var cardID = CardID{GUID: [16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xff},
}
if err := yk.SetCardID(DefaultManagementKey, &cardID); err != nil {
t.Fatal(err)
}
newCID, err := yk.CardID()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(newCID.GUID[:], cardID.GUID[:]) {
t.Errorf("(*CardID, got=0x%x, want=0x%x", newCID.GUID, cardID.GUID)
}
}