pax_global_header00006660000000000000000000000064140717356340014524gustar00rootroot0000000000000052 comment=23adea5d745fded4128735d10e1843346aa7a707 piv-go-1.8.0/000077500000000000000000000000001407173563400127335ustar00rootroot00000000000000piv-go-1.8.0/.github/000077500000000000000000000000001407173563400142735ustar00rootroot00000000000000piv-go-1.8.0/.github/workflows/000077500000000000000000000000001407173563400163305ustar00rootroot00000000000000piv-go-1.8.0/.github/workflows/test.yaml000066400000000000000000000017061407173563400201770ustar00rootroot00000000000000name: test on: pull_request: branches: - master jobs: build: name: Linux runs-on: ubuntu-latest steps: - name: Set up Go 1.14 uses: actions/setup-go@v2 with: go-version: '^1.14.2' id: go - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Install golint run: go get -u golang.org/x/lint/golint - name: Install libpcsc run: sudo apt-get install -y libpcsclite-dev pcscd pcsc-tools - name: Test run: "make test" build-windows: name: Windows runs-on: windows-latest steps: - name: Set up Go 1.14 uses: actions/setup-go@v2 with: go-version: '^1.14.2' id: go - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Install golint run: go get -u golang.org/x/lint/golint - name: Test run: "make build" env: CGO_ENABLED: 0 piv-go-1.8.0/CONTRIBUTING.md000066400000000000000000000021111407173563400151570ustar00rootroot00000000000000# How to Contribute We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. ## Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. ## Community Guidelines This project follows [Google's Open Source Community Guidelines](https://opensource.google/conduct/). piv-go-1.8.0/LICENSE000066400000000000000000000261361407173563400137500ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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 http://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. piv-go-1.8.0/Makefile000066400000000000000000000002101407173563400143640ustar00rootroot00000000000000.PHONY: test test: lint go test -v ./... .PHONY: lint lint: golint -set_exit_status ./... .PHONY: build build: lint go build ./... piv-go-1.8.0/README.md000066400000000000000000000176501407173563400142230ustar00rootroot00000000000000This is not an officially supported Google product # A Go YubiKey PIV implementation [![GoDoc](https://godoc.org/github.com/go-piv/piv-go/piv?status.svg)](https://godoc.org/github.com/go-piv/piv-go/piv) YubiKeys implement the PIV specification for managing smart card certificates. This applet is a simpler alternative to GPG for managing asymmetric keys on a YubiKey. This package is an alternative to Paul Tagliamonte's [go-ykpiv](https://github.com/paultag/go-ykpiv), a wrapper for YubiKey's ykpiv.h C library. This package aims to provide: * Better error messages * Idiomatic Go APIs * Modern features such as PIN protected management keys ## Examples * [Signing](#signing) * [PINs](#pins) * [Certificates](#certificates) * [Attestation](#attestation) ### Signing The piv-go package can be used to generate keys and store certificates on a YubiKey. This uses a management key to generate new keys on the applet, and a PIN for signing operations. The package provides default PIN values. If the PIV credentials on the YubiKey haven't been modified, the follow code generates a new EC key on the smartcard, and provides a signing interface: ```go // List all smartcards connected to the system. cards, err := piv.Cards() if err != nil { // ... } // Find a YubiKey and open the reader. var yk *piv.YubiKey for _, card := range cards { if strings.Contains(strings.ToLower(card), "yubikey") { if yk, err = piv.Open(card); err != nil { // ... } break } } if yk == nil { // ... } // Generate a private key on the YubiKey. key := piv.Key{ Algorithm: piv.AlgorithmEC256, PINPolicy: piv.PINPolicyAlways, TouchPolicy: piv.TouchPolicyAlways, } pub, err := yk.GenerateKey(piv.DefaultManagementKey, piv.SlotAuthentication, key) if err != nil { // ... } auth := piv.KeyAuth{PIN: piv.DefaultPIN} priv, err := yk.PrivateKey(piv.SlotAuthentication, pub, auth) if err != nil { // ... } // Use private key to sign or decrypt. ``` ### PINs The PIV applet has three unique credentials: * Management key (3DES key) used to generate new keys on the YubiKey. * PIN (up to 8 digits, usually 6) used to access signing operations. * PUK (up to 8 digits) used to unblock the PIN. Usually set once and thrown away or managed by an administrator. piv-go implements PIN protected management keys to store the management key on the YubiKey. This allows users to only provide a PIN and still access management capabilities. The following code generates new, random credentials for a YubiKey: ```go newPINInt, err := rand.Int(rand.Reader, big.NewInt(1_000_000)) if err != nil { // ... } newPUKInt, err := rand.Int(rand.Reader, big.NewInt(100_000_000)) if err != nil { // ... } var newKey [24]byte if _, err := io.ReadFull(rand.Reader, newKey[:]); err != nil { // ... } // Format with leading zeros. newPIN := fmt.Sprintf("%06d", newPINInt) newPUK := fmt.Sprintf("%08d", newPUKInt) // Set all values to a new value. if err := yk.SetManagementKey(piv.DefaultManagementKey, newKey); err != nil { // ... } if err := yk.SetPUK(piv.DefaultPUK, newPUK); err != nil { // ... } if err := yk.SetPIN(piv.DefaultPIN, newPIN); err != nil { // ... } // Store management key on the YubiKey. m := piv.Metadata{ManagementKey: &newKey} if err := yk.SetMetadata(newKey, m); err != nil { // ... } fmt.Println("Credentials set. Your PIN is: %s", newPIN) ``` The user can user the PIN later to fetch the management key: ```go m, err := yk.Metadata(pin) if err != nil { // ... } if m.ManagementKey == nil { // ... } key := *m.ManagementKey ``` ### Certificates The PIV applet can also store X.509 certificates on the YubiKey: ```go cert, err := x509.ParseCertificate(certDER) if err != nil { // ... } if err := yk.SetCertificate(managementKey, piv.SlotAuthentication, cert); err != nil { // ... } ``` The certificate can later be used in combination with the private key. For example, to serve TLS traffic: ```go cert, err := yk.Certificate(piv.SlotAuthentication) if err != nil { // ... } priv, err := yk.PrivateKey(piv.SlotAuthentication, cert.PublicKey, auth) if err != nil { // ... } s := &http.Server{ TLSConfig: &tls.Config{ Certificates: []tls.Certificate{ { Certificate: [][]byte{cert.Raw}, PrivateKey: priv, }, }, }, Handler: myHandler, } ``` ### Attestation YubiKeys can attest that a particular key was generated on the smartcard, and that it was set with specific PIN and touch policies. The client generates a key, then asks the YubiKey to sign an attestation certificate: ```go // Get the YubiKey's attestation certificate, which is signed by Yubico. yubiKeyAttestationCert, err := yk.AttestationCertificate() if err != nil { // ... } // Generate a key on the YubiKey and generate an attestation certificate for // that key. This will be signed by the YubiKey's attestation certificate. key := piv.Key{ Algorithm: piv.AlgorithmEC256, PINPolicy: piv.PINPolicyAlways, TouchPolicy: piv.TouchPolicyAlways, } if _, err := yk.GenerateKey(managementKey, piv.SlotAuthentication, key); err != nil { // ... } slotAttestationCertificate, err := yk.Attest(piv.SlotAuthentication) if err != nil { // ... } // Send certificates to server. ``` A CA can then verify the attestation, proving a key was generated on the card and enforce policy: ```go // Server receives both certificates, then proves a key was generated on the // YubiKey. a, err := piv.Verify(yubiKeyAttestationCert, slotAttestationCertificate) if err != nil { // ... } if a.TouchPolicy != piv.TouchPolicyAlways { // ... } // Record YubiKey's serial number and public key. pub := slotAttestationCertificate.PublicKey serial := a.Serial ``` ## Installation On MacOS, piv-go doesn't require any additional packages. To build on Linux, piv-go requires PCSC lite. To install on Debian-based distros, run: ``` sudo apt-get install libpcsclite-dev ``` On Fedora: ``` sudo yum install pcsc-lite-devel ``` On CentOS: ``` sudo yum install 'dnf-command(config-manager)' sudo yum config-manager --set-enabled PowerTools sudo yum install pcsc-lite-devel ``` On FreeBSD: ``` sudo pkg install pcsc-lite ``` On Windows: No prerequisites are needed. The default driver by Microsoft supports all functionalities which get tested by unittests. However if you run into problems try the official [YubiKey Smart Card Minidriver](https://www.yubico.com/products/services-software/download/smart-card-drivers-tools/). Yubico states on their website the driver adds [_additional smart functionality_](https://www.yubico.com/authentication-standards/smart-card/). Please notice the following: >Windows support is best effort due to lack of test hardware. This means the maintainers will take patches for Windows, but if you encounter a bug or the build is broken, you may be asked to fix it. ## Non-YubiKey smartcards Non-YubiKey smartcards that implement the PIV standard are not officially supported due to a lack of test hardware. However, PRs that fix integrations with other smartcards are welcome, and piv-go will attempt to not break that support. ## Testing Tests automatically find connected available YubiKeys, but won't modify the smart card without the `--wipe-yubikey` flag. To let the tests modify your YubiKey's PIV applet, run: ``` go test -v ./piv --wipe-yubikey ``` Longer tests can be skipped with the `--test.short` flag. ``` go test -v --short ./piv --wipe-yubikey ``` ## Why? YubiKey's C PIV library, ykpiv, is brittle. The error messages aren't terrific, and while it has debug options, plumbing them through isn't idiomatic or convenient. ykpiv wraps PC/SC APIs available on Windows, Mac, and Linux. There's no requirement for it to be written in any particular langauge. As an alternative to [pault.ag/go/ykpiv][go-ykpiv] this package re-implements ykpiv in Go instead of calling it. ## Alternatives OpenSSH has experimental support for U2F keys ([announcement][openssh-u2f]) that directly use browser U2F challenges for smart cards. [go-ykpiv]: https://github.com/paultag/go-ykpiv [openssh-u2f]: https://marc.info/?l=openssh-unix-dev&m=157259802529972&w=2 piv-go-1.8.0/go.mod000066400000000000000000000000511407173563400140350ustar00rootroot00000000000000module github.com/go-piv/piv-go go 1.13 piv-go-1.8.0/piv/000077500000000000000000000000001407173563400135315ustar00rootroot00000000000000piv-go-1.8.0/piv/doc.go000066400000000000000000000012501407173563400146230ustar00rootroot00000000000000// Copyright 2020 Google LLC // // 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 implements management functionality for the YubiKey PIV applet. package piv piv-go-1.8.0/piv/key.go000066400000000000000000001144411407173563400146550ustar00rootroot00000000000000// Copyright 2020 Google LLC // // 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 ( "bytes" "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" "errors" "fmt" "io" "math/big" "strconv" "strings" ) // errMismatchingAlgorithms is returned when a cryptographic operation // is given keys using different algorithms. var errMismatchingAlgorithms = errors.New("mismatching key algorithms") // errUnsupportedKeySize is returned when a key has an unsupported size var errUnsupportedKeySize = errors.New("unsupported key size") // unsupportedCurveError is used when a key has an unsupported curve type unsupportedCurveError struct { curve int } func (e unsupportedCurveError) Error() string { return fmt.Sprintf("unsupported curve: %d", e.curve) } // Slot is a private key and certificate combination managed by the security key. type Slot struct { // Key is a reference for a key type. // // See: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=32 Key uint32 // Object is a reference for data object. // // See: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=30 Object uint32 } var ( extIDFirmwareVersion = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 41482, 3, 3}) extIDSerialNumber = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 41482, 3, 7}) extIDKeyPolicy = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 41482, 3, 8}) extIDFormFactor = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 41482, 3, 9}) ) // Version encodes a major, minor, and patch version. type Version struct { Major int Minor int Patch int } // Formfactor enumerates the physical set of forms a key can take. USB-A vs. // USB-C and Keychain vs. Nano. type Formfactor int // Formfactors recognized by this package. const ( FormfactorUSBAKeychain = iota + 1 FormfactorUSBANano FormfactorUSBCKeychain FormfactorUSBCNano FormfactorUSBCLightningKeychain ) // Prefix in the x509 Subject Common Name for YubiKey attestations // https://developers.yubico.com/PIV/Introduction/PIV_attestation.html const yubikeySubjectCNPrefix = "YubiKey PIV Attestation " // Attestation returns additional information about a key attested to be generated // on a card. See https://developers.yubico.com/PIV/Introduction/PIV_attestation.html // for more information. type Attestation struct { // Version of the YubiKey's firmware. Version Version // Serial is the YubiKey's serial number. Serial uint32 // Formfactor indicates the physical type of the YubiKey. // // Formfactor may be empty Formfactor(0) for some YubiKeys. Formfactor Formfactor // PINPolicy set on the slot. PINPolicy PINPolicy // TouchPolicy set on the slot. TouchPolicy TouchPolicy // Slot is the inferred slot the attested key resides in based on the // common name in the attestation. If the slot cannot be determined, // this field will be an empty struct. Slot Slot } func (a *Attestation) addExt(e pkix.Extension) error { if e.Id.Equal(extIDFirmwareVersion) { if len(e.Value) != 3 { return fmt.Errorf("expected 3 bytes for firmware version, got: %d", len(e.Value)) } a.Version = Version{ Major: int(e.Value[0]), Minor: int(e.Value[1]), Patch: int(e.Value[2]), } } else if e.Id.Equal(extIDSerialNumber) { var serial int64 if _, err := asn1.Unmarshal(e.Value, &serial); err != nil { return fmt.Errorf("parsing serial number: %v", err) } if serial < 0 { return fmt.Errorf("serial number was negative: %d", serial) } a.Serial = uint32(serial) } else if e.Id.Equal(extIDKeyPolicy) { if len(e.Value) != 2 { return fmt.Errorf("expected 2 bytes from key policy, got: %d", len(e.Value)) } switch e.Value[0] { case 0x01: a.PINPolicy = PINPolicyNever case 0x02: a.PINPolicy = PINPolicyOnce case 0x03: a.PINPolicy = PINPolicyAlways default: return fmt.Errorf("unrecognized pin policy: 0x%x", e.Value[0]) } switch e.Value[1] { case 0x01: a.TouchPolicy = TouchPolicyNever case 0x02: a.TouchPolicy = TouchPolicyAlways case 0x03: a.TouchPolicy = TouchPolicyCached default: return fmt.Errorf("unrecognized touch policy: 0x%x", e.Value[1]) } } else if e.Id.Equal(extIDFormFactor) { if len(e.Value) != 1 { return fmt.Errorf("expected 1 byte from formfactor, got: %d", len(e.Value)) } switch e.Value[0] { case 0x01: a.Formfactor = FormfactorUSBAKeychain case 0x02: a.Formfactor = FormfactorUSBANano case 0x03: a.Formfactor = FormfactorUSBCKeychain case 0x04: a.Formfactor = FormfactorUSBCNano case 0x05: a.Formfactor = FormfactorUSBCLightningKeychain default: return fmt.Errorf("unrecognized formfactor: 0x%x", e.Value[0]) } } return nil } func verifySignature(parent, c *x509.Certificate) error { return parent.CheckSignature(c.SignatureAlgorithm, c.RawTBSCertificate, c.Signature) } // Verify proves that a key was generated on a YubiKey. It ensures the slot and // YubiKey certificate chains up to the Yubico CA, parsing additional information // out of the slot certificate, such as the touch and PIN policies of a key. func Verify(attestationCert, slotCert *x509.Certificate) (*Attestation, error) { var v verifier return v.Verify(attestationCert, slotCert) } type verifier struct { Root *x509.Certificate } func (v *verifier) Verify(attestationCert, slotCert *x509.Certificate) (*Attestation, error) { root := v.Root if root == nil { ca, err := yubicoCA() if err != nil { return nil, fmt.Errorf("parsing yubico ca: %v", err) } root = ca } if err := verifySignature(root, attestationCert); err != nil { return nil, fmt.Errorf("attestation certifcate not signed by : %v", err) } if err := verifySignature(attestationCert, slotCert); err != nil { return nil, fmt.Errorf("slot certificate not signed by attestation certifcate: %v", err) } return parseAttestation(slotCert) } func parseAttestation(slotCert *x509.Certificate) (*Attestation, error) { var a Attestation for _, ext := range slotCert.Extensions { if err := a.addExt(ext); err != nil { return nil, fmt.Errorf("parsing extension: %v", err) } } slot, ok := parseSlot(slotCert.Subject.CommonName) if ok { a.Slot = slot } return &a, nil } func parseSlot(commonName string) (Slot, bool) { if !strings.HasPrefix(commonName, yubikeySubjectCNPrefix) { return Slot{}, false } slotName := strings.TrimPrefix(commonName, yubikeySubjectCNPrefix) key, err := strconv.ParseUint(slotName, 16, 32) if err != nil { return Slot{}, false } switch uint32(key) { case SlotAuthentication.Key: return SlotAuthentication, true case SlotSignature.Key: return SlotSignature, true case SlotCardAuthentication.Key: return SlotCardAuthentication, true case SlotKeyManagement.Key: return SlotKeyManagement, true } return RetiredKeyManagementSlot(uint32(key)) } // yubicoPIVCAPEM is the PEM encoded attestation certificate used by Yubico. // // https://developers.yubico.com/PIV/Introduction/PIV_attestation.html const yubicoPIVCAPEM = `-----BEGIN CERTIFICATE----- MIIDFzCCAf+gAwIBAgIDBAZHMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNVBAMMIFl1 YmljbyBQSVYgUm9vdCBDQSBTZXJpYWwgMjYzNzUxMCAXDTE2MDMxNDAwMDAwMFoY DzIwNTIwNDE3MDAwMDAwWjArMSkwJwYDVQQDDCBZdWJpY28gUElWIFJvb3QgQ0Eg U2VyaWFsIDI2Mzc1MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMN2 cMTNR6YCdcTFRxuPy31PabRn5m6pJ+nSE0HRWpoaM8fc8wHC+Tmb98jmNvhWNE2E ilU85uYKfEFP9d6Q2GmytqBnxZsAa3KqZiCCx2LwQ4iYEOb1llgotVr/whEpdVOq joU0P5e1j1y7OfwOvky/+AXIN/9Xp0VFlYRk2tQ9GcdYKDmqU+db9iKwpAzid4oH BVLIhmD3pvkWaRA2H3DA9t7H/HNq5v3OiO1jyLZeKqZoMbPObrxqDg+9fOdShzgf wCqgT3XVmTeiwvBSTctyi9mHQfYd2DwkaqxRnLbNVyK9zl+DzjSGp9IhVPiVtGet X02dxhQnGS7K6BO0Qe8CAwEAAaNCMEAwHQYDVR0OBBYEFMpfyvLEojGc6SJf8ez0 1d8Cv4O/MA8GA1UdEwQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 DQEBCwUAA4IBAQBc7Ih8Bc1fkC+FyN1fhjWioBCMr3vjneh7MLbA6kSoyWF70N3s XhbXvT4eRh0hvxqvMZNjPU/VlRn6gLVtoEikDLrYFXN6Hh6Wmyy1GTnspnOvMvz2 lLKuym9KYdYLDgnj3BeAvzIhVzzYSeU77/Cupofj093OuAswW0jYvXsGTyix6B3d bW5yWvyS9zNXaqGaUmP3U9/b6DlHdDogMLu3VLpBB9bm5bjaKWWJYgWltCVgUbFq Fqyi4+JE014cSgR57Jcu3dZiehB6UtAPgad9L5cNvua/IWRmm+ANy3O2LH++Pyl8 SREzU8onbBsjMg9QDiSf5oJLKvd/Ren+zGY7 -----END CERTIFICATE-----` func yubicoCA() (*x509.Certificate, error) { b, _ := pem.Decode([]byte(yubicoPIVCAPEM)) if b == nil { return nil, fmt.Errorf("failed to decode yubico pem data") } return x509.ParseCertificate(b.Bytes) } // Slot combinations pre-defined by this package. // // Object IDs are specified in NIST 800-73-4 section 4.3: // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=30 // // Key IDs are specified in NIST 800-73-4 section 5.1: // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=32 var ( SlotAuthentication = Slot{0x9a, 0x5fc105} SlotSignature = Slot{0x9c, 0x5fc10a} SlotCardAuthentication = Slot{0x9e, 0x5fc101} SlotKeyManagement = Slot{0x9d, 0x5fc10b} slotAttestation = Slot{0xf9, 0x5fff01} ) var retiredKeyManagementSlots = map[uint32]Slot{ 0x82: {0x82, 0x5fc10d}, 0x83: {0x83, 0x5fc10e}, 0x84: {0x84, 0x5fc10f}, 0x85: {0x85, 0x5fc110}, 0x86: {0x86, 0x5fc111}, 0x87: {0x87, 0x5fc112}, 0x88: {0x88, 0x5fc113}, 0x89: {0x89, 0x5fc114}, 0x8a: {0x8a, 0x5fc115}, 0x8b: {0x8b, 0x5fc116}, 0x8c: {0x8c, 0x5fc117}, 0x8d: {0x8d, 0x5fc118}, 0x8e: {0x8e, 0x5fc119}, 0x8f: {0x8f, 0x5fc11a}, 0x90: {0x90, 0x5fc11b}, 0x91: {0x91, 0x5fc11c}, 0x92: {0x92, 0x5fc11d}, 0x93: {0x93, 0x5fc11e}, 0x94: {0x94, 0x5fc11f}, 0x95: {0x95, 0x5fc120}, } // RetiredKeyManagementSlot provides access to "retired" slots. Slots meant for old Key Management // keys that have been rotated. YubiKeys 4 and later support values between 0x82 and 0x95 (inclusive). // // slot, ok := RetiredKeyManagementSlot(0x82) // if !ok { // // unrecognized slot // } // pub, err := yk.GenerateKey(managementKey, slot, key) // // https://developers.yubico.com/PIV/Introduction/Certificate_slots.html#_slot_82_95_retired_key_management func RetiredKeyManagementSlot(key uint32) (Slot, bool) { slot, ok := retiredKeyManagementSlots[key] return slot, ok } // String returns the two-character hex representation of the slot func (s Slot) String() string { return strconv.FormatUint(uint64(s.Key), 16) } // Algorithm represents a specific algorithm and bit size supported by the PIV // specification. type Algorithm int // Algorithms supported by this package. Note that not all cards will support // every algorithm. // // AlgorithmEd25519 is currently only implemented by SoloKeys. // // For algorithm discovery, see: https://github.com/ericchiang/piv-go/issues/1 const ( AlgorithmEC256 Algorithm = iota + 1 AlgorithmEC384 AlgorithmEd25519 AlgorithmRSA1024 AlgorithmRSA2048 ) // PINPolicy represents PIN requirements when signing or decrypting with an // asymmetric key in a given slot. type PINPolicy int // PIN policies supported by this package. // // BUG(ericchiang): Caching for PINPolicyOnce isn't supported on YubiKey // versions older than 4.3.0 due to issues with verifying if a PIN is needed. // If specified, a PIN will be required for every operation. const ( PINPolicyNever PINPolicy = iota + 1 PINPolicyOnce PINPolicyAlways ) // TouchPolicy represents proof-of-presence requirements when signing or // decrypting with asymmetric key in a given slot. type TouchPolicy int // Touch policies supported by this package. const ( TouchPolicyNever TouchPolicy = iota + 1 TouchPolicyAlways TouchPolicyCached ) const ( tagPINPolicy = 0xaa tagTouchPolicy = 0xab ) var pinPolicyMap = map[PINPolicy]byte{ PINPolicyNever: 0x01, PINPolicyOnce: 0x02, PINPolicyAlways: 0x03, } var touchPolicyMap = map[TouchPolicy]byte{ TouchPolicyNever: 0x01, TouchPolicyAlways: 0x02, TouchPolicyCached: 0x03, } var algorithmsMap = map[Algorithm]byte{ AlgorithmEC256: algECCP256, AlgorithmEC384: algECCP384, AlgorithmEd25519: algEd25519, AlgorithmRSA1024: algRSA1024, AlgorithmRSA2048: algRSA2048, } // AttestationCertificate returns the YubiKey's attestation certificate, which // is unique to the key and signed by Yubico. func (yk *YubiKey) AttestationCertificate() (*x509.Certificate, error) { return yk.Certificate(slotAttestation) } // Attest generates a certificate for a key, signed by the YubiKey's attestation // certificate. This can be used to prove a key was generate on a specific // YubiKey. // // This method is only supported for YubiKey versions >= 4.3.0. // https://developers.yubico.com/PIV/Introduction/PIV_attestation.html // // Certificates returned by this method MUST NOT be used for anything other than // attestion or determining the slots public key. For example, the certificate // is NOT suitable for TLS. // // If the slot doesn't have a key, the returned error wraps ErrNotFound. func (yk *YubiKey) Attest(slot Slot) (*x509.Certificate, error) { cert, err := ykAttest(yk.tx, slot) if err == nil { return cert, nil } var e *apduErr if errors.As(err, &e) && e.sw1 == 0x6A && e.sw2 == 0x80 { return nil, ErrNotFound } return nil, err } func ykAttest(tx *scTx, slot Slot) (*x509.Certificate, error) { cmd := apdu{ instruction: insAttest, param1: byte(slot.Key), } resp, err := tx.Transmit(cmd) if err != nil { return nil, fmt.Errorf("command failed: %w", err) } if bytes.HasPrefix(resp, []byte{0x70}) { b, _, err := unmarshalASN1(resp, 0, 0x10) // tag 0x70 if err != nil { return nil, fmt.Errorf("unmarshaling certificate: %v", err) } resp = b } cert, err := x509.ParseCertificate(resp) if err != nil { return nil, fmt.Errorf("parsing certificate: %v", err) } return cert, nil } // Certificate returns the certifiate object stored in a given slot. // // If a certificate hasn't been set in the provided slot, the returned error // wraps ErrNotFound. func (yk *YubiKey) Certificate(slot Slot) (*x509.Certificate, error) { cmd := apdu{ instruction: insGetData, param1: 0x3f, param2: 0xff, data: []byte{ 0x5c, // Tag list 0x03, // Length of tag byte(slot.Object >> 16), byte(slot.Object >> 8), byte(slot.Object), }, } resp, err := yk.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) } certDER, _, err := unmarshalASN1(obj, 1, 0x10) // tag 0x70 if err != nil { return nil, fmt.Errorf("unmarshaling certificate: %v", err) } cert, err := x509.ParseCertificate(certDER) if err != nil { return nil, fmt.Errorf("parsing certificate: %v", err) } return cert, nil } // marshalASN1Length encodes the length. func marshalASN1Length(n uint64) []byte { var l []byte if n < 0x80 { l = []byte{byte(n)} } else if n < 0x100 { l = []byte{0x81, byte(n)} } else { l = []byte{0x82, byte(n >> 8), byte(n)} } return l } // marshalASN1 encodes a tag, length and data. // // TODO: clean this up and maybe switch to cryptobyte? func marshalASN1(tag byte, data []byte) []byte { l := marshalASN1Length(uint64(len(data))) d := append([]byte{tag}, l...) return append(d, data...) } // SetCertificate stores a certificate object in the provided slot. Setting a // certificate isn't required to use the associated key for signing or // decryption. func (yk *YubiKey) SetCertificate(key [24]byte, slot Slot, cert *x509.Certificate) error { if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil { return fmt.Errorf("authenticating with management key: %w", err) } return ykStoreCertificate(yk.tx, slot, cert) } func ykStoreCertificate(tx *scTx, slot Slot, cert *x509.Certificate) error { // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=40 data := marshalASN1(0x70, cert.Raw) // "for a certificate encoded in uncompressed form CertInfo shall be 0x00" data = append(data, marshalASN1(0x71, []byte{0x00})...) // Error Detection Code data = append(data, marshalASN1(0xfe, nil)...) // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=94 data = append([]byte{ 0x5c, // Tag list 0x03, // Length of tag byte(slot.Object >> 16), byte(slot.Object >> 8), byte(slot.Object), }, marshalASN1(0x53, data)...) cmd := apdu{ instruction: insPutData, param1: 0x3f, param2: 0xff, data: data, } if _, err := tx.Transmit(cmd); err != nil { return fmt.Errorf("command failed: %v", err) } return nil } // Key is used for key generation and holds different options for the key. // // While keys can have default PIN and touch policies, this package currently // doesn't support this option, and all fields must be provided. type Key struct { // Algorithm to use when generating the key. Algorithm Algorithm // PINPolicy for the key. // // BUG(ericchiang): some older YubiKeys (third generation) will silently // drop this value. If PINPolicyNever or PINPolicyOnce is supplied but the // key still requires a PIN every time, you may be using a buggy key and // should supply PINPolicyAlways. See https://github.com/go-piv/piv-go/issues/60 PINPolicy PINPolicy // TouchPolicy for the key. TouchPolicy TouchPolicy } // GenerateKey generates an asymmetric key on the card, returning the key's // public key. func (yk *YubiKey) GenerateKey(key [24]byte, slot Slot, opts Key) (crypto.PublicKey, error) { if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil { return nil, fmt.Errorf("authenticating with management key: %w", err) } return ykGenerateKey(yk.tx, slot, opts) } func ykGenerateKey(tx *scTx, slot Slot, o Key) (crypto.PublicKey, error) { alg, ok := algorithmsMap[o.Algorithm] if !ok { return nil, fmt.Errorf("unsupported algorithm") } tp, ok := touchPolicyMap[o.TouchPolicy] if !ok { return nil, fmt.Errorf("unsupported touch policy") } pp, ok := pinPolicyMap[o.PINPolicy] if !ok { return nil, fmt.Errorf("unsupported pin policy") } // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=95 cmd := apdu{ instruction: insGenerateAsymmetric, param2: byte(slot.Key), data: []byte{ 0xac, 0x09, // length of remaining data algTag, 0x01, alg, tagPINPolicy, 0x01, pp, tagTouchPolicy, 0x01, tp, }, } resp, err := tx.Transmit(cmd) if err != nil { return nil, fmt.Errorf("command failed: %w", err) } var curve elliptic.Curve switch o.Algorithm { case AlgorithmRSA1024, AlgorithmRSA2048: pub, err := decodeRSAPublic(resp) if err != nil { return nil, fmt.Errorf("decoding rsa public key: %v", err) } return pub, nil case AlgorithmEC256: curve = elliptic.P256() case AlgorithmEC384: curve = elliptic.P384() case AlgorithmEd25519: pub, err := decodeEd25519Public(resp) if err != nil { return nil, fmt.Errorf("decoding ed25519 public key: %v", err) } return pub, nil default: return nil, fmt.Errorf("unsupported algorithm") } pub, err := decodeECPublic(resp, curve) if err != nil { return nil, fmt.Errorf("decoding ec public key: %v", err) } return pub, nil } // KeyAuth is used to authenticate against the YubiKey on each signing and // decryption request. type KeyAuth struct { // PIN, if provided, is a static PIN used to authenticate against the key. // If provided, PINPrompt is ignored. PIN string // PINPrompt can be used to interactively request the PIN from the user. The // method is only called when needed. For example, if a key specifies // PINPolicyOnce, PINPrompt will only be called once per YubiKey struct. PINPrompt func() (pin string, err error) // PINPolicy can be used to specify the PIN caching strategy for the slot. If // not provided, this will be inferred from the attestation certificate. // // This field is required on older (<4.3.0) YubiKeys when using PINPrompt, // as well as for keys imported to the card. PINPolicy PINPolicy } func isAuthErr(err error) bool { var e *apduErr if !errors.As(err, &e) { return false } return e.sw1 == 0x69 && e.sw2 == 0x82 // "security status not satisfied" } func (k KeyAuth) authTx(yk *YubiKey, pp PINPolicy) error { // PINPolicyNever shouldn't require a PIN. if pp == PINPolicyNever { return nil } // PINPolicyAlways should always prompt a PIN even if the key says that // login isn't needed. // https://github.com/go-piv/piv-go/issues/49 if pp != PINPolicyAlways && !ykLoginNeeded(yk.tx) { return nil } pin := k.PIN if pin == "" && k.PINPrompt != nil { p, err := k.PINPrompt() if err != nil { return fmt.Errorf("pin prompt: %v", err) } pin = p } if pin == "" { return fmt.Errorf("pin required but wasn't provided") } return ykLogin(yk.tx, pin) } func (k KeyAuth) do(yk *YubiKey, pp PINPolicy, f func(tx *scTx) ([]byte, error)) ([]byte, error) { if err := k.authTx(yk, pp); err != nil { return nil, err } return f(yk.tx) } func pinPolicy(yk *YubiKey, slot Slot) (PINPolicy, error) { cert, err := yk.Attest(slot) if err != nil { var e *apduErr if errors.As(err, &e) && e.sw1 == 0x6d && e.sw2 == 0x00 { // Attestation cert command not supported, probably an older YubiKey. // Guess PINPolicyAlways. // // See https://github.com/go-piv/piv-go/issues/55 return PINPolicyAlways, nil } return 0, fmt.Errorf("get attestation cert: %v", err) } a, err := parseAttestation(cert) if err != nil { return 0, fmt.Errorf("parse attestation cert: %v", err) } if _, ok := pinPolicyMap[a.PINPolicy]; ok { return a.PINPolicy, nil } return PINPolicyOnce, nil } // PrivateKey is used to access signing and decryption options for the key // stored in the slot. The returned key implements crypto.Signer and/or // crypto.Decrypter depending on the key type. // // If the public key hasn't been stored externally, it can be provided by // fetching the slot's attestation certificate: // // cert, err := yk.Attest(slot) // if err != nil { // // ... // } // priv, err := yk.PrivateKey(slot, cert.PublicKey, auth) // func (yk *YubiKey) PrivateKey(slot Slot, public crypto.PublicKey, auth KeyAuth) (crypto.PrivateKey, error) { pp := PINPolicyNever if _, ok := pinPolicyMap[auth.PINPolicy]; ok { // If the PIN policy is manually specified, trust that value instead of // trying to use the attestation certificate. pp = auth.PINPolicy } else if auth.PIN != "" || auth.PINPrompt != nil { // Attempt to determine the key's PIN policy. This helps inform the // strategy for when to prompt for a PIN. policy, err := pinPolicy(yk, slot) if err != nil { return nil, err } pp = policy } switch pub := public.(type) { case *ecdsa.PublicKey: return &ECDSAPrivateKey{yk, slot, pub, auth, pp}, nil case ed25519.PublicKey: return &keyEd25519{yk, slot, pub, auth, pp}, nil case *rsa.PublicKey: return &keyRSA{yk, slot, pub, auth, pp}, nil default: return nil, fmt.Errorf("unsupported public key type: %T", public) } } // SetPrivateKeyInsecure is an insecure method which imports a private key into the slot. // Users should almost always use GeneratePrivateKey() instead. // // Importing a private key breaks functionality provided by this package, including // AttestationCertificate() and Attest(). There are no stability guarantees for other // methods for imported private keys. // // Keys generated outside of the YubiKey should not be considered hardware-backed, // as there's no way to prove the key wasn't copied, exfiltrated, or replaced with malicious // material before being imported. func (yk *YubiKey) SetPrivateKeyInsecure(key [24]byte, slot Slot, private crypto.PrivateKey, policy Key) error { // Reference implementation // https://github.com/Yubico/yubico-piv-tool/blob/671a5740ef09d6c5d9d33f6e5575450750b58bde/lib/ykpiv.c#L1812 params := make([][]byte, 0) var paramTag byte var elemLen int switch priv := private.(type) { case *rsa.PrivateKey: paramTag = 0x01 switch priv.N.BitLen() { case 1024: policy.Algorithm = AlgorithmRSA1024 elemLen = 64 case 2048: policy.Algorithm = AlgorithmRSA2048 elemLen = 128 default: return errUnsupportedKeySize } priv.Precompute() params = append(params, priv.Primes[0].Bytes()) // P params = append(params, priv.Primes[1].Bytes()) // Q params = append(params, priv.Precomputed.Dp.Bytes()) // dP params = append(params, priv.Precomputed.Dq.Bytes()) // dQ params = append(params, priv.Precomputed.Qinv.Bytes()) // Qinv case *ecdsa.PrivateKey: paramTag = 0x6 size := priv.PublicKey.Params().BitSize switch size { case 256: policy.Algorithm = AlgorithmEC256 elemLen = 32 case 384: policy.Algorithm = AlgorithmEC384 elemLen = 48 default: return unsupportedCurveError{curve: size} } // S value privateKey := make([]byte, elemLen) valueBytes := priv.D.Bytes() padding := len(privateKey) - len(valueBytes) copy(privateKey[padding:], valueBytes) params = append(params, privateKey) default: return errors.New("unsupported private key type") } elemLenASN1 := marshalASN1Length(uint64(elemLen)) tags := make([]byte, 0) for i, param := range params { tag := paramTag + byte(i) tags = append(tags, tag) tags = append(tags, elemLenASN1...) padding := elemLen - len(param) param = append(make([]byte, padding), param...) tags = append(tags, param...) } if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil { return fmt.Errorf("authenticating with management key: %w", err) } return ykImportKey(yk.tx, tags, slot, policy) } func ykImportKey(tx *scTx, tags []byte, slot Slot, o Key) error { alg, ok := algorithmsMap[o.Algorithm] if !ok { return fmt.Errorf("unsupported algorithm") } tp, ok := touchPolicyMap[o.TouchPolicy] if !ok { return fmt.Errorf("unsupported touch policy") } pp, ok := pinPolicyMap[o.PINPolicy] if !ok { return fmt.Errorf("unsupported pin policy") } // This command is a Yubico PIV extension. // https://developers.yubico.com/PIV/Introduction/Yubico_extensions.html cmd := apdu{ instruction: insImportKey, param1: alg, param2: byte(slot.Key), data: append(tags, []byte{ tagPINPolicy, 0x01, pp, tagTouchPolicy, 0x01, tp, }...), } if _, err := tx.Transmit(cmd); err != nil { return fmt.Errorf("command failed: %w", err) } return nil } // ECDSAPrivateKey is a crypto.PrivateKey implementation for ECDSA // keys. It implements crypto.Signer and the method SharedKey performs // Diffie-Hellman key agreements. // // Keys returned by YubiKey.PrivateKey() may be type asserted to // *ECDSAPrivateKey, if the slot contains an ECDSA key. type ECDSAPrivateKey struct { yk *YubiKey slot Slot pub *ecdsa.PublicKey auth KeyAuth pp PINPolicy } // Public returns the public key associated with this private key. func (k *ECDSAPrivateKey) Public() crypto.PublicKey { return k.pub } var _ crypto.Signer = (*ECDSAPrivateKey)(nil) // Sign implements crypto.Signer. func (k *ECDSAPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { return k.auth.do(k.yk, k.pp, func(tx *scTx) ([]byte, error) { return ykSignECDSA(tx, k.slot, k.pub, digest) }) } // SharedKey performs a Diffie-Hellman key agreement with the peer // to produce a shared secret key. // // Peer's public key must use the same algorithm as the key in // this slot, or an error will be returned. // // Length of the result depends on the types and sizes of the keys // used for the operation. Callers should use a cryptographic key // derivation function to extract the amount of bytes they need. func (k *ECDSAPrivateKey) SharedKey(peer *ecdsa.PublicKey) ([]byte, error) { if peer.Curve.Params().BitSize != k.pub.Curve.Params().BitSize { return nil, errMismatchingAlgorithms } msg := elliptic.Marshal(peer.Curve, peer.X, peer.Y) return k.auth.do(k.yk, k.pp, func(tx *scTx) ([]byte, error) { var alg byte size := k.pub.Params().BitSize switch size { case 256: alg = algECCP256 case 384: alg = algECCP384 default: return nil, unsupportedCurveError{curve: size} } // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=118 // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=93 cmd := apdu{ instruction: insAuthenticate, param1: alg, param2: byte(k.slot.Key), data: marshalASN1(0x7c, append([]byte{0x82, 0x00}, marshalASN1(0x85, msg)...)), } resp, err := tx.Transmit(cmd) if err != nil { return nil, fmt.Errorf("command failed: %w", err) } sig, _, err := unmarshalASN1(resp, 1, 0x1c) // 0x7c if err != nil { return nil, fmt.Errorf("unmarshal response: %v", err) } rs, _, err := unmarshalASN1(sig, 2, 0x02) // 0x82 if err != nil { return nil, fmt.Errorf("unmarshal response signature: %v", err) } return rs, nil }) } type keyEd25519 struct { yk *YubiKey slot Slot pub ed25519.PublicKey auth KeyAuth pp PINPolicy } func (k *keyEd25519) Public() crypto.PublicKey { return k.pub } func (k *keyEd25519) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { return k.auth.do(k.yk, k.pp, func(tx *scTx) ([]byte, error) { return skSignEd25519(tx, k.slot, k.pub, digest) }) } type keyRSA struct { yk *YubiKey slot Slot pub *rsa.PublicKey auth KeyAuth pp PINPolicy } func (k *keyRSA) Public() crypto.PublicKey { return k.pub } func (k *keyRSA) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { return k.auth.do(k.yk, k.pp, func(tx *scTx) ([]byte, error) { return ykSignRSA(tx, k.slot, k.pub, digest, opts) }) } func (k *keyRSA) Decrypt(rand io.Reader, msg []byte, opts crypto.DecrypterOpts) ([]byte, error) { return k.auth.do(k.yk, k.pp, func(tx *scTx) ([]byte, error) { return ykDecryptRSA(tx, k.slot, k.pub, msg) }) } func ykSignECDSA(tx *scTx, slot Slot, pub *ecdsa.PublicKey, digest []byte) ([]byte, error) { var alg byte size := pub.Params().BitSize switch size { case 256: alg = algECCP256 case 384: alg = algECCP384 default: return nil, unsupportedCurveError{curve: size} } // Same as the standard library // https://github.com/golang/go/blob/go1.13.5/src/crypto/ecdsa/ecdsa.go#L125-L128 orderBytes := (size + 7) / 8 if len(digest) > orderBytes { digest = digest[:orderBytes] } // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=118 cmd := apdu{ instruction: insAuthenticate, param1: alg, param2: byte(slot.Key), data: marshalASN1(0x7c, append([]byte{0x82, 0x00}, marshalASN1(0x81, digest)...)), } resp, err := tx.Transmit(cmd) if err != nil { return nil, fmt.Errorf("command failed: %w", err) } sig, _, err := unmarshalASN1(resp, 1, 0x1c) // 0x7c if err != nil { return nil, fmt.Errorf("unmarshal response: %v", err) } rs, _, err := unmarshalASN1(sig, 2, 0x02) // 0x82 if err != nil { return nil, fmt.Errorf("unmarshal response signature: %v", err) } return rs, nil } // This function only works on SoloKeys prototypes and other PIV devices that choose // to implement Ed25519 signatures under alg 0x22. func skSignEd25519(tx *scTx, slot Slot, pub ed25519.PublicKey, digest []byte) ([]byte, error) { // Adaptation of // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=118 cmd := apdu{ instruction: insAuthenticate, param1: algEd25519, param2: byte(slot.Key), data: marshalASN1(0x7c, append([]byte{0x82, 0x00}, marshalASN1(0x81, digest)...)), } resp, err := tx.Transmit(cmd) if err != nil { return nil, fmt.Errorf("command failed: %w", err) } sig, _, err := unmarshalASN1(resp, 1, 0x1c) // 0x7c if err != nil { return nil, fmt.Errorf("unmarshal response: %v", err) } rs, _, err := unmarshalASN1(sig, 2, 0x02) // 0x82 if err != nil { return nil, fmt.Errorf("unmarshal response signature: %v", err) } return rs, nil } func unmarshalASN1(b []byte, class, tag int) (obj, rest []byte, err error) { var v asn1.RawValue rest, err = asn1.Unmarshal(b, &v) if err != nil { return nil, nil, err } if v.Class != class || v.Tag != tag { return nil, nil, fmt.Errorf("unexpected class=%d and tag=0x%x", v.Class, v.Tag) } return v.Bytes, rest, nil } func decodeECPublic(b []byte, curve elliptic.Curve) (*ecdsa.PublicKey, error) { // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=95 r, _, err := unmarshalASN1(b, 1, 0x49) if err != nil { return nil, fmt.Errorf("unmarshal response: %v", err) } p, _, err := unmarshalASN1(r, 2, 0x06) if err != nil { return nil, fmt.Errorf("unmarshal points: %v", err) } // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=96 size := curve.Params().BitSize / 8 if len(p) != (size*2)+1 { return nil, fmt.Errorf("unexpected points length: %d", len(p)) } // Are points uncompressed? if p[0] != 0x04 { return nil, fmt.Errorf("points were not uncompressed") } p = p[1:] var x, y big.Int x.SetBytes(p[:size]) y.SetBytes(p[size:]) if !curve.IsOnCurve(&x, &y) { return nil, fmt.Errorf("resulting points are not on curve") } return &ecdsa.PublicKey{Curve: curve, X: &x, Y: &y}, nil } func decodeEd25519Public(b []byte) (ed25519.PublicKey, error) { // Adaptation of // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=95 r, _, err := unmarshalASN1(b, 1, 0x49) if err != nil { return nil, fmt.Errorf("unmarshal response: %v", err) } p, _, err := unmarshalASN1(r, 2, 0x06) if err != nil { return nil, fmt.Errorf("unmarshal points: %v", err) } if len(p) != ed25519.PublicKeySize { return nil, fmt.Errorf("unexpected points length: %d", len(p)) } return ed25519.PublicKey(p), nil } func decodeRSAPublic(b []byte) (*rsa.PublicKey, error) { // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=95 r, _, err := unmarshalASN1(b, 1, 0x49) if err != nil { return nil, fmt.Errorf("unmarshal response: %v", err) } mod, r, err := unmarshalASN1(r, 2, 0x01) if err != nil { return nil, fmt.Errorf("unmarshal modulus: %v", err) } exp, _, err := unmarshalASN1(r, 2, 0x02) if err != nil { return nil, fmt.Errorf("unmarshal exponent: %v", err) } var n, e big.Int n.SetBytes(mod) e.SetBytes(exp) if !e.IsInt64() { return nil, fmt.Errorf("returned exponent too large: %s", e.String()) } return &rsa.PublicKey{N: &n, E: int(e.Int64())}, nil } func rsaAlg(pub *rsa.PublicKey) (byte, error) { size := pub.N.BitLen() switch size { case 1024: return algRSA1024, nil case 2048: return algRSA2048, nil default: return 0, fmt.Errorf("unsupported rsa key size: %d", size) } } func ykDecryptRSA(tx *scTx, slot Slot, pub *rsa.PublicKey, data []byte) ([]byte, error) { alg, err := rsaAlg(pub) if err != nil { return nil, err } cmd := apdu{ instruction: insAuthenticate, param1: alg, param2: byte(slot.Key), data: marshalASN1(0x7c, append([]byte{0x82, 0x00}, marshalASN1(0x81, data)...)), } resp, err := tx.Transmit(cmd) if err != nil { return nil, fmt.Errorf("command failed: %w", err) } sig, _, err := unmarshalASN1(resp, 1, 0x1c) // 0x7c if err != nil { return nil, fmt.Errorf("unmarshal response: %v", err) } decrypted, _, err := unmarshalASN1(sig, 2, 0x02) // 0x82 if err != nil { return nil, fmt.Errorf("unmarshal response signature: %v", err) } // Decrypted blob contains a bunch of random data. Look for a NULL byte which // indicates where the plain text starts. for i := 2; i+1 < len(decrypted); i++ { if decrypted[i] == 0x00 { return decrypted[i+1:], nil } } return nil, fmt.Errorf("invalid pkcs#1 v1.5 padding") } // PKCS#1 v15 is largely informed by the standard library // https://github.com/golang/go/blob/go1.13.5/src/crypto/rsa/pkcs1v15.go func ykSignRSA(tx *scTx, slot Slot, pub *rsa.PublicKey, digest []byte, opts crypto.SignerOpts) ([]byte, error) { if _, ok := opts.(*rsa.PSSOptions); ok { return nil, fmt.Errorf("rsassa-pss signatures not supported") } alg, err := rsaAlg(pub) if err != nil { return nil, err } hash := opts.HashFunc() if hash.Size() != len(digest) { return nil, fmt.Errorf("input must be a hashed message") } prefix, ok := hashPrefixes[hash] if !ok { return nil, fmt.Errorf("unsupported hash algorithm: crypto.Hash(%d)", hash) } // https://tools.ietf.org/pdf/rfc2313.pdf#page=9 d := make([]byte, len(prefix)+len(digest)) copy(d[:len(prefix)], prefix) copy(d[len(prefix):], digest) paddingLen := pub.Size() - 3 - len(d) if paddingLen < 0 { return nil, fmt.Errorf("message too large") } padding := make([]byte, paddingLen) for i := range padding { padding[i] = 0xff } // https://tools.ietf.org/pdf/rfc2313.pdf#page=9 data := append([]byte{0x00, 0x01}, padding...) data = append(data, 0x00) data = append(data, d...) // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=117 cmd := apdu{ instruction: insAuthenticate, param1: alg, param2: byte(slot.Key), data: marshalASN1(0x7c, append([]byte{0x82, 0x00}, marshalASN1(0x81, data)...)), } resp, err := tx.Transmit(cmd) if err != nil { return nil, fmt.Errorf("command failed: %w", err) } sig, _, err := unmarshalASN1(resp, 1, 0x1c) // 0x7c if err != nil { return nil, fmt.Errorf("unmarshal response: %v", err) } pkcs1v15Sig, _, err := unmarshalASN1(sig, 2, 0x02) // 0x82 if err != nil { return nil, fmt.Errorf("unmarshal response signature: %v", err) } return pkcs1v15Sig, nil } var hashPrefixes = map[crypto.Hash][]byte{ crypto.MD5: {0x30, 0x20, 0x30, 0x0c, 0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05, 0x05, 0x00, 0x04, 0x10}, crypto.SHA1: {0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14}, crypto.SHA224: {0x30, 0x2d, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, 0x05, 0x00, 0x04, 0x1c}, crypto.SHA256: {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}, crypto.SHA384: {0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30}, crypto.SHA512: {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40}, crypto.MD5SHA1: {}, // A special TLS case which doesn't use an ASN1 prefix. crypto.RIPEMD160: {0x30, 0x20, 0x30, 0x08, 0x06, 0x06, 0x28, 0xcf, 0x06, 0x03, 0x00, 0x31, 0x04, 0x14}, } piv-go-1.8.0/piv/key_test.go000066400000000000000000000532051407173563400157140ustar00rootroot00000000000000// Copyright 2020 Google LLC // // 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 ( "bytes" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "errors" "math/big" "testing" "time" ) func TestYubiKeySignECDSA(t *testing.T) { yk, close := newTestYubiKey(t) defer close() if err := yk.Reset(); err != nil { t.Fatalf("reset yubikey: %v", err) } slot := SlotAuthentication key := Key{ Algorithm: AlgorithmEC256, TouchPolicy: TouchPolicyNever, PINPolicy: PINPolicyNever, } pubKey, err := yk.GenerateKey(DefaultManagementKey, slot, key) if err != nil { t.Fatalf("generating key: %v", err) } pub, ok := pubKey.(*ecdsa.PublicKey) if !ok { t.Fatalf("public key is not an ecdsa key") } data := sha256.Sum256([]byte("hello")) priv, err := yk.PrivateKey(slot, pub, KeyAuth{}) if err != nil { t.Fatalf("getting private key: %v", err) } s, ok := priv.(crypto.Signer) if !ok { t.Fatalf("expected private key to implement crypto.Signer") } out, err := s.Sign(rand.Reader, data[:], crypto.SHA256) if err != nil { t.Fatalf("signing failed: %v", err) } var sig struct { R, S *big.Int } if _, err := asn1.Unmarshal(out, &sig); err != nil { t.Fatalf("unmarshaling signature: %v", err) } if !ecdsa.Verify(pub, data[:], sig.R, sig.S) { t.Errorf("signature didn't match") } } func TestYubiKeyECDSASharedKey(t *testing.T) { yk, close := newTestYubiKey(t) defer close() slot := SlotAuthentication key := Key{ Algorithm: AlgorithmEC256, TouchPolicy: TouchPolicyNever, PINPolicy: PINPolicyNever, } pubKey, err := yk.GenerateKey(DefaultManagementKey, slot, key) if err != nil { t.Fatalf("generating key: %v", err) } pub, ok := pubKey.(*ecdsa.PublicKey) if !ok { t.Fatalf("public key is not an ecdsa key") } priv, err := yk.PrivateKey(slot, pub, KeyAuth{}) if err != nil { t.Fatalf("getting private key: %v", err) } privECDSA, ok := priv.(*ECDSAPrivateKey) if !ok { t.Fatalf("expected private key to be ECDSA private key") } t.Run("good", func(t *testing.T) { eph, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("cannot generate key: %v", err) } mult, _ := pub.ScalarMult(pub.X, pub.Y, eph.D.Bytes()) secret1 := mult.Bytes() secret2, err := privECDSA.SharedKey(&eph.PublicKey) if err != nil { t.Fatalf("key agreement failed: %v", err) } if !bytes.Equal(secret1, secret2) { t.Errorf("key agreement didn't match") } }) t.Run("bad", func(t *testing.T) { t.Run("size", func(t *testing.T) { eph, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) if err != nil { t.Fatalf("cannot generate key: %v", err) } _, err = privECDSA.SharedKey(&eph.PublicKey) if !errors.Is(err, errMismatchingAlgorithms) { t.Fatalf("unexpected error value: wanted errMismatchingAlgorithms: %v", err) } }) }) } func TestPINPrompt(t *testing.T) { tests := []struct { name string policy PINPolicy want int }{ {"Never", PINPolicyNever, 0}, {"Once", PINPolicyOnce, 1}, {"Always", PINPolicyAlways, 2}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { yk, close := newTestYubiKey(t) defer close() k := Key{ Algorithm: AlgorithmEC256, PINPolicy: test.policy, TouchPolicy: TouchPolicyNever, } pub, err := yk.GenerateKey(DefaultManagementKey, SlotAuthentication, k) if err != nil { t.Fatalf("generating key on slot: %v", err) } got := 0 auth := KeyAuth{ PINPrompt: func() (string, error) { got++ return DefaultPIN, nil }, } if !supportsAttestation(yk) { auth.PINPolicy = test.policy } priv, err := yk.PrivateKey(SlotAuthentication, pub, auth) if err != nil { t.Fatalf("building private key: %v", err) } s, ok := priv.(crypto.Signer) if !ok { t.Fatalf("expected crypto.Signer got %T", priv) } data := sha256.Sum256([]byte("foo")) if _, err := s.Sign(rand.Reader, data[:], crypto.SHA256); err != nil { t.Errorf("signing error: %v", err) } if _, err := s.Sign(rand.Reader, data[:], crypto.SHA256); err != nil { t.Errorf("signing error: %v", err) } if got != test.want { t.Errorf("PINPrompt called %d times, want=%d", got, test.want) } }) } } func supportsAttestation(yk *YubiKey) bool { return supportsVersion(yk.Version(), 4, 3, 0) } func TestSlots(t *testing.T) { yk, close := newTestYubiKey(t) if err := yk.Reset(); err != nil { t.Fatalf("resetting yubikey: %v", err) } close() tests := []struct { name string slot Slot }{ {"Authentication", SlotAuthentication}, {"CardAuthentication", SlotCardAuthentication}, {"KeyManagement", SlotKeyManagement}, {"Signature", SlotSignature}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { yk, close := newTestYubiKey(t) defer close() if supportsAttestation(yk) { if _, err := yk.Attest(test.slot); err == nil || !errors.Is(err, ErrNotFound) { t.Errorf("attest: got err=%v, want=ErrNotFound", err) } } k := Key{ Algorithm: AlgorithmEC256, PINPolicy: PINPolicyNever, TouchPolicy: TouchPolicyNever, } pub, err := yk.GenerateKey(DefaultManagementKey, test.slot, k) if err != nil { t.Fatalf("generating key on slot: %v", err) } if supportsAttestation(yk) { if _, err := yk.Attest(test.slot); err != nil { t.Errorf("attest: %v", err) } } priv, err := yk.PrivateKey(test.slot, pub, KeyAuth{PIN: DefaultPIN}) if err != nil { t.Fatalf("private key: %v", err) } tmpl := &x509.Certificate{ Subject: pkix.Name{CommonName: "my-client"}, SerialNumber: big.NewInt(1), NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } raw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, priv) if err != nil { t.Fatalf("signing self-signed certificate: %v", err) } cert, err := x509.ParseCertificate(raw) if err != nil { t.Fatalf("parse certificate: %v", err) } if _, err := yk.Certificate(test.slot); err == nil || !errors.Is(err, ErrNotFound) { t.Errorf("get certificate, got err=%v, want=ErrNotFound", err) } if err := yk.SetCertificate(DefaultManagementKey, test.slot, cert); err != nil { t.Fatalf("set certificate: %v", err) } got, err := yk.Certificate(test.slot) if err != nil { t.Fatalf("get certifiate: %v", err) } if !bytes.Equal(got.Raw, raw) { t.Errorf("certificate from slot didn't match the certificate written") } }) } } func TestYubiKeySignRSA(t *testing.T) { tests := []struct { name string alg Algorithm long bool }{ {"rsa1024", AlgorithmRSA1024, false}, {"rsa2048", AlgorithmRSA2048, true}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.long && testing.Short() { t.Skip("skipping test in short mode") } yk, close := newTestYubiKey(t) defer close() slot := SlotAuthentication key := Key{ Algorithm: test.alg, TouchPolicy: TouchPolicyNever, PINPolicy: PINPolicyNever, } pubKey, err := yk.GenerateKey(DefaultManagementKey, slot, key) if err != nil { t.Fatalf("generating key: %v", err) } pub, ok := pubKey.(*rsa.PublicKey) if !ok { t.Fatalf("public key is not an rsa key") } data := sha256.Sum256([]byte("hello")) priv, err := yk.PrivateKey(slot, pub, KeyAuth{}) if err != nil { t.Fatalf("getting private key: %v", err) } s, ok := priv.(crypto.Signer) if !ok { t.Fatalf("private key didn't implement crypto.Signer") } out, err := s.Sign(rand.Reader, data[:], crypto.SHA256) if err != nil { t.Fatalf("signing failed: %v", err) } if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, data[:], out); err != nil { t.Errorf("failed to verify signature: %v", err) } }) } } func TestYubiKeyDecryptRSA(t *testing.T) { tests := []struct { name string alg Algorithm long bool }{ {"rsa1024", AlgorithmRSA1024, false}, {"rsa2048", AlgorithmRSA2048, true}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.long && testing.Short() { t.Skip("skipping test in short mode") } yk, close := newTestYubiKey(t) defer close() slot := SlotAuthentication key := Key{ Algorithm: test.alg, TouchPolicy: TouchPolicyNever, PINPolicy: PINPolicyNever, } pubKey, err := yk.GenerateKey(DefaultManagementKey, slot, key) if err != nil { t.Fatalf("generating key: %v", err) } pub, ok := pubKey.(*rsa.PublicKey) if !ok { t.Fatalf("public key is not an rsa key") } data := []byte("hello") ct, err := rsa.EncryptPKCS1v15(rand.Reader, pub, data) if err != nil { t.Fatalf("encryption failed: %v", err) } priv, err := yk.PrivateKey(slot, pub, KeyAuth{}) if err != nil { t.Fatalf("getting private key: %v", err) } d, ok := priv.(crypto.Decrypter) if !ok { t.Fatalf("private key didn't implement crypto.Decypter") } got, err := d.Decrypt(rand.Reader, ct, nil) if err != nil { t.Fatalf("decryption failed: %v", err) } if !bytes.Equal(data, got) { t.Errorf("decrypt, got=%q, want=%q", got, data) } }) } } func TestYubiKeyAttestation(t *testing.T) { yk, close := newTestYubiKey(t) defer close() key := Key{ Algorithm: AlgorithmEC256, PINPolicy: PINPolicyNever, TouchPolicy: TouchPolicyNever, } testRequiresVersion(t, yk, 4, 3, 0) cert, err := yk.AttestationCertificate() if err != nil { t.Fatalf("getting attestation certificate: %v", err) } pub, err := yk.GenerateKey(DefaultManagementKey, SlotAuthentication, key) if err != nil { t.Fatalf("generate key: %v", err) } _ = pub c, err := yk.Attest(SlotAuthentication) if err != nil { t.Fatalf("attesting key: %v", err) } a, err := Verify(cert, c) if err != nil { t.Fatalf("failed to verify attestation: %v", err) } serial, err := yk.Serial() if err != nil { t.Errorf("getting serial number: %v", err) } else if a.Serial != serial { t.Errorf("attestation serial got=%d, wanted=%d", a.Serial, serial) } if a.PINPolicy != key.PINPolicy { t.Errorf("attestation pin policy got=0x%x, wanted=0x%x", a.TouchPolicy, key.PINPolicy) } if a.TouchPolicy != key.TouchPolicy { t.Errorf("attestation touch policy got=0x%x, wanted=0x%x", a.TouchPolicy, key.TouchPolicy) } if a.Version != yk.Version() { t.Errorf("attestation version got=%#v, wanted=%#v", a.Version, yk.Version()) } if a.Slot != SlotAuthentication { t.Errorf("attested slot got=%v, wanted=%v", a.Slot, SlotAuthentication) } if a.Slot.String() != "9a" { t.Errorf("attested slot name got=%s, wanted=%s", a.Slot.String(), "9a") } } func TestYubiKeyStoreCertificate(t *testing.T) { yk, close := newTestYubiKey(t) defer close() slot := SlotAuthentication caPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("generating ca private: %v", err) } // Generate a self-signed certificate caTmpl := &x509.Certificate{ Subject: pkix.Name{CommonName: "my-ca"}, SerialNumber: big.NewInt(100), BasicConstraintsValid: true, IsCA: true, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, } caCertDER, err := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, caPriv.Public(), caPriv) if err != nil { t.Fatalf("generating self-signed certificate: %v", err) } caCert, err := x509.ParseCertificate(caCertDER) if err != nil { t.Fatalf("parsing ca cert: %v", err) } key := Key{ Algorithm: AlgorithmEC256, TouchPolicy: TouchPolicyNever, PINPolicy: PINPolicyNever, } pub, err := yk.GenerateKey(DefaultManagementKey, slot, key) if err != nil { t.Fatalf("generating key: %v", err) } cliTmpl := &x509.Certificate{ Subject: pkix.Name{CommonName: "my-client"}, SerialNumber: big.NewInt(101), NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } cliCertDER, err := x509.CreateCertificate(rand.Reader, cliTmpl, caCert, pub, caPriv) if err != nil { t.Fatalf("creating client cert: %v", err) } cliCert, err := x509.ParseCertificate(cliCertDER) if err != nil { t.Fatalf("parsing cli cert: %v", err) } if err := yk.SetCertificate(DefaultManagementKey, slot, cliCert); err != nil { t.Fatalf("storing client cert: %v", err) } gotCert, err := yk.Certificate(slot) if err != nil { t.Fatalf("getting client cert: %v", err) } if !bytes.Equal(gotCert.Raw, cliCert.Raw) { t.Errorf("stored cert didn't match cert retrieved") } } func TestYubiKeyGenerateKey(t *testing.T) { tests := []struct { name string alg Algorithm bits int long bool // Does the key generation take a long time? }{ { name: "ec_256", alg: AlgorithmEC256, }, { name: "ec_384", alg: AlgorithmEC384, }, { name: "rsa_1024", alg: AlgorithmRSA1024, }, { name: "rsa_2048", alg: AlgorithmRSA2048, long: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.long && testing.Short() { t.Skip("skipping test in short mode") } yk, close := newTestYubiKey(t) defer close() if test.alg == AlgorithmEC384 { testRequiresVersion(t, yk, 4, 3, 0) } key := Key{ Algorithm: test.alg, TouchPolicy: TouchPolicyNever, PINPolicy: PINPolicyNever, } if _, err := yk.GenerateKey(DefaultManagementKey, SlotAuthentication, key); err != nil { t.Errorf("generating key: %v", err) } }) } } func TestYubiKeyPrivateKey(t *testing.T) { alg := AlgorithmEC256 slot := SlotAuthentication yk, close := newTestYubiKey(t) defer close() key := Key{ Algorithm: alg, TouchPolicy: TouchPolicyNever, PINPolicy: PINPolicyNever, } pub, err := yk.GenerateKey(DefaultManagementKey, slot, key) if err != nil { t.Fatalf("generating key: %v", err) } ecdsaPub, ok := pub.(*ecdsa.PublicKey) if !ok { t.Fatalf("public key is not an *ecdsa.PublicKey: %T", pub) } auth := KeyAuth{PIN: DefaultPIN} priv, err := yk.PrivateKey(slot, pub, auth) if err != nil { t.Fatalf("getting private key: %v", err) } signer, ok := priv.(crypto.Signer) if !ok { t.Fatalf("private key doesn't implement crypto.Signer") } b := sha256.Sum256([]byte("hello")) hash := b[:] sig, err := signer.Sign(rand.Reader, hash, crypto.SHA256) if err != nil { t.Fatalf("signing failed: %v", err) } var ecdsaSignature struct { R, S *big.Int } if _, err := asn1.Unmarshal(sig, &ecdsaSignature); err != nil { t.Fatalf("unmarshal: %v", err) } if !ecdsa.Verify(ecdsaPub, hash, ecdsaSignature.R, ecdsaSignature.S) { t.Fatalf("signature validation failed") } } func TestYubiKeyPrivateKeyPINError(t *testing.T) { alg := AlgorithmEC256 slot := SlotAuthentication yk, close := newTestYubiKey(t) defer close() key := Key{ Algorithm: alg, TouchPolicy: TouchPolicyNever, PINPolicy: PINPolicyAlways, } pub, err := yk.GenerateKey(DefaultManagementKey, slot, key) if err != nil { t.Fatalf("generating key: %v", err) } auth := KeyAuth{ PINPrompt: func() (string, error) { return "", errors.New("test error") }, } priv, err := yk.PrivateKey(slot, pub, auth) if err != nil { t.Fatalf("getting private key: %v", err) } signer, ok := priv.(crypto.Signer) if !ok { t.Fatalf("private key doesn't implement crypto.Signer") } b := sha256.Sum256([]byte("hello")) hash := b[:] if _, err := signer.Sign(rand.Reader, hash, crypto.SHA256); err == nil { t.Errorf("expected sign to fail with pin prompt that returned error") } } func TestRetiredKeyManagementSlot(t *testing.T) { tests := []struct { name string key uint32 wantSlot Slot wantOk bool }{ { name: "Non-existent slot, before range", key: 0x0, wantSlot: Slot{}, wantOk: false, }, { name: "Non-existent slot, after range", key: 0x96, wantSlot: Slot{}, wantOk: false, }, { name: "First retired slot key", key: 0x82, wantSlot: Slot{0x82, 0x5fc10d}, wantOk: true, }, { name: "Last retired slot key", key: 0x95, wantSlot: Slot{0x95, 0x5fc120}, wantOk: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotSlot, gotOk := RetiredKeyManagementSlot(tt.key) if gotSlot != tt.wantSlot { t.Errorf("RetiredKeyManagementSlot() got = %v, want %v", gotSlot, tt.wantSlot) } if gotOk != tt.wantOk { t.Errorf("RetiredKeyManagementSlot() got1 = %v, want %v", gotOk, tt.wantOk) } }) } } func TestSetRSAPrivateKey(t *testing.T) { tests := []struct { name string bits int slot Slot wantErr error }{ { name: "rsa 1024", bits: 1024, slot: SlotSignature, wantErr: nil, }, { name: "rsa 2048", bits: 2048, slot: SlotCardAuthentication, wantErr: nil, }, { name: "rsa 4096", bits: 4096, slot: SlotAuthentication, wantErr: errUnsupportedKeySize, }, { name: "rsa 512", bits: 512, slot: SlotKeyManagement, wantErr: errUnsupportedKeySize, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { yk, close := newTestYubiKey(t) defer close() generated, err := rsa.GenerateKey(rand.Reader, tt.bits) if err != nil { t.Fatalf("generating private key: %v", err) } err = yk.SetPrivateKeyInsecure(DefaultManagementKey, tt.slot, generated, Key{ PINPolicy: PINPolicyNever, TouchPolicy: TouchPolicyNever, }) if err != tt.wantErr { t.Fatalf("SetPrivateKeyInsecure(): wantErr=%v, got err=%v", tt.wantErr, err) } if err != nil { return } priv, err := yk.PrivateKey(tt.slot, &generated.PublicKey, KeyAuth{}) if err != nil { t.Fatalf("getting private key: %v", err) } data := []byte("Test data that we will encrypt") // Encrypt the data using our generated key encrypted, err := rsa.EncryptPKCS1v15(rand.Reader, &generated.PublicKey, data) if err != nil { t.Fatalf("encrypting data: %v", err) } deviceDecrypter := priv.(crypto.Decrypter) // Decrypt the data on the device decrypted, err := deviceDecrypter.Decrypt(rand.Reader, encrypted, nil) if err != nil { t.Fatalf("decrypting data: %v", err) } if bytes.Compare(data, decrypted) != 0 { t.Fatalf("decrypted data is different to the source data") } }) } } func TestSetECDSAPrivateKey(t *testing.T) { tests := []struct { name string curve elliptic.Curve slot Slot wantErr error }{ { name: "ecdsa P256", curve: elliptic.P256(), slot: SlotSignature, wantErr: nil, }, { name: "ecdsa P384", curve: elliptic.P384(), slot: SlotCardAuthentication, wantErr: nil, }, { name: "ecdsa P224", curve: elliptic.P224(), slot: SlotAuthentication, wantErr: unsupportedCurveError{curve: 224}, }, { name: "ecdsa P521", curve: elliptic.P521(), slot: SlotKeyManagement, wantErr: unsupportedCurveError{curve: 521}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { yk, close := newTestYubiKey(t) defer close() generated, err := ecdsa.GenerateKey(tt.curve, rand.Reader) if err != nil { t.Fatalf("generating private key: %v", err) } err = yk.SetPrivateKeyInsecure(DefaultManagementKey, tt.slot, generated, Key{ PINPolicy: PINPolicyNever, TouchPolicy: TouchPolicyNever, }) if err != tt.wantErr { t.Fatalf("SetPrivateKeyInsecure(): wantErr=%v, got err=%v", tt.wantErr, err) } if err != nil { return } priv, err := yk.PrivateKey(tt.slot, &generated.PublicKey, KeyAuth{}) if err != nil { t.Fatalf("getting private key: %v", err) } deviceSigner := priv.(crypto.Signer) hash := []byte("Test data to sign") // Sign the data on the device sig, err := deviceSigner.Sign(rand.Reader, hash, nil) if err != nil { t.Fatalf("signing data: %v", err) } // Verify the signature using the generated key if !ecdsa.VerifyASN1(&generated.PublicKey, hash, sig) { t.Fatal("Failed to verify signed data") } }) } } func TestParseSlot(t *testing.T) { tests := []struct { name string cn string ok bool slot Slot }{ { name: "Missing Yubico PIV Prefix", cn: "invalid", ok: false, slot: Slot{}, }, { name: "Invalid Slot Name", cn: yubikeySubjectCNPrefix + "xy", ok: false, slot: Slot{}, }, { name: "Valid -- SlotAuthentication", cn: yubikeySubjectCNPrefix + "9a", ok: true, slot: SlotAuthentication, }, { name: "Valid -- Retired Management Key", cn: yubikeySubjectCNPrefix + "89", ok: true, slot: retiredKeyManagementSlots[uint32(137)], }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { slot, ok := parseSlot(test.cn) if ok != test.ok { t.Errorf("ok status returned %v, expected %v", ok, test.ok) } if slot != test.slot { t.Errorf("returned slot %+v did not match expected %+v", slot, test.slot) } }) } } piv-go-1.8.0/piv/pcsc.go000066400000000000000000000114151407173563400150120ustar00rootroot00000000000000// Copyright 2020 Google LLC // // 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 ( "errors" "fmt" ) type scErr struct { // rc holds the return code for a given call. rc int64 } func (e *scErr) Error() string { if msg, ok := pcscErrMsgs[e.rc]; ok { return msg } return fmt.Sprintf("unknown pcsc return code 0x%08x", e.rc) } // AuthErr is an error indicating an authentication error occurred (wrong PIN or blocked). type AuthErr struct { // Retries is the number of retries remaining if this error resulted from a retriable // authentication attempt. If the authentication method is blocked or does not support // retries, this will be 0. Retries int } func (v AuthErr) Error() string { r := "retries" if v.Retries == 1 { r = "retry" } return fmt.Sprintf("verification failed (%d %s remaining)", v.Retries, r) } // ErrNotFound is returned when the requested object on the smart card is not found. var ErrNotFound = errors.New("data object or application not found") // apduErr is an error interacting with the PIV application on the smart card. // This error may wrap more accessible errors, like ErrNotFound or an instance // of AuthErr, so callers are encouraged to use errors.Is and errors.As for // these common cases. type apduErr struct { sw1 byte sw2 byte } // Status returns the Status Word returned by the card command. func (a *apduErr) Status() uint16 { return uint16(a.sw1)<<8 | uint16(a.sw2) } func (a *apduErr) Error() string { var msg string if u := a.Unwrap(); u != nil { msg = u.Error() } switch a.Status() { // 0x6300 is "verification failed", represented as AuthErr{0} // 0x63Cn is "verification failed" with retry, represented as AuthErr{n} case 0x6882: msg = "secure messaging not supported" case 0x6982: msg = "security status not satisfied" case 0x6983: // This will also be AuthErr{0} but we override the message here // so that it's clear that the reason is a block rather than a simple // failed authentication verification. msg = "authentication method blocked" case 0x6987: msg = "expected secure messaging data objects are missing" case 0x6988: msg = "secure messaging data objects are incorrect" case 0x6a80: msg = "incorrect parameter in command data field" case 0x6a81: msg = "function not supported" // 0x6a82 is "data object or application not found" aka ErrNotFound case 0x6a84: msg = "not enough memory" case 0x6a86: msg = "incorrect parameter in P1 or P2" case 0x6a88: msg = "referenced data or reference data not found" } if msg != "" { msg = ": " + msg } return fmt.Sprintf("smart card error %04x%s", a.Status(), msg) } // Unwrap retrieves an accessible error type, if able. func (a *apduErr) Unwrap() error { st := a.Status() switch { case st == 0x6a82: return ErrNotFound case st == 0x6300: return AuthErr{0} case st == 0x6983: return AuthErr{0} case st&0xfff0 == 0x63c0: return AuthErr{int(st & 0xf)} case st&0xfff0 == 0x6300: // Older YubiKeys sometimes return sw1=0x63 and sw2=0x0N to indicate the // number of retries. This isn't spec compliant, but support it anyway. // // https://github.com/go-piv/piv-go/issues/60 return AuthErr{int(st & 0xf)} } return nil } type apdu struct { instruction byte param1 byte param2 byte data []byte } func (t *scTx) Transmit(d apdu) ([]byte, error) { data := d.data var resp []byte const maxAPDUDataSize = 0xff for len(data) > maxAPDUDataSize { req := make([]byte, 5+maxAPDUDataSize) req[0] = 0x10 // ISO/IEC 7816-4 5.1.1 req[1] = d.instruction req[2] = d.param1 req[3] = d.param2 req[4] = 0xff copy(req[5:], data[:maxAPDUDataSize]) data = data[maxAPDUDataSize:] _, r, err := t.transmit(req) if err != nil { return nil, fmt.Errorf("transmitting initial chunk %w", err) } resp = append(resp, r...) } req := make([]byte, 5+len(data)) req[1] = d.instruction req[2] = d.param1 req[3] = d.param2 req[4] = byte(len(data)) copy(req[5:], data) hasMore, r, err := t.transmit(req) if err != nil { return nil, err } resp = append(resp, r...) for hasMore { req := make([]byte, 5) req[1] = insGetResponseAPDU var r []byte hasMore, r, err = t.transmit(req) if err != nil { return nil, fmt.Errorf("reading further response: %w", err) } resp = append(resp, r...) } return resp, nil } piv-go-1.8.0/piv/pcsc_darwin.go000066400000000000000000000021361407173563400163560ustar00rootroot00000000000000// Copyright 2020 Google LLC // // 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 "C" func scCheck(rc C.int) error { if rc == rcSuccess { return nil } i := int64(rc) if i < 0 { // On MacOS, int isn't big enough to handle the return codes so the // leading bit becomes a two's complement bit. If the return code is // negative, correct this. // https://github.com/go-piv/piv-go/issues/53 i += (1 << 32) } return &scErr{i} } func isRCNoReaders(rc C.int) bool { // MacOS does the right thing and doesn't return an error if no smart cards // are available. return false } piv-go-1.8.0/piv/pcsc_errors000066400000000000000000000130741407173563400160050ustar00rootroot00000000000000SCARD_S_SUCCESS 0x00000000 No error was encountered. SCARD_F_INTERNAL_ERROR 0x80100001 An internal consistency check failed. SCARD_E_CANCELLED 0x80100002 The action was cancelled by an SCardCancel request. SCARD_E_INVALID_HANDLE 0x80100003 The supplied handle was invalid. SCARD_E_INVALID_PARAMETER 0x80100004 One or more of the supplied parameters could not be properly interpreted. SCARD_E_INVALID_TARGET 0x80100005 Registry startup information is missing or invalid. SCARD_E_NO_MEMORY 0x80100006 Not enough memory available to complete this command. SCARD_F_WAITED_TOO_LONG 0x80100007 An internal consistency timer has expired. SCARD_E_INSUFFICIENT_BUFFER 0x80100008 The data buffer to receive returned data is too small for the returned data. SCARD_E_UNKNOWN_READER 0x80100009 The specified reader name is not recognized. SCARD_E_TIMEOUT 0x8010000A The user-specified timeout value has expired. SCARD_E_SHARING_VIOLATION 0x8010000B The smart card cannot be accessed because of other connections outstanding. SCARD_E_NO_SMARTCARD 0x8010000C The operation requires a Smart Card, but no Smart Card is currently in the device. SCARD_E_UNKNOWN_CARD 0x8010000D The specified smart card name is not recognized. SCARD_E_CANT_DISPOSE 0x8010000E The system could not dispose of the media in the requested manner. SCARD_E_PROTO_MISMATCH 0x8010000F The requested protocols are incompatible with the protocol currently in use with the smart card. SCARD_E_NOT_READY 0x80100010 The reader or smart card is not ready to accept commands. SCARD_E_INVALID_VALUE 0x80100011 One or more of the supplied parameters values could not be properly interpreted. SCARD_E_SYSTEM_CANCELLED 0x80100012 The action was cancelled by the system, presumably to log off or shut down. SCARD_F_COMM_ERROR 0x80100013 An internal communications error has been detected. SCARD_F_UNKNOWN_ERROR 0x80100014 An internal error has been detected, but the source is unknown. SCARD_E_INVALID_ATR 0x80100015 An ATR obtained from the registry is not a valid ATR string. SCARD_E_NOT_TRANSACTED 0x80100016 An attempt was made to end a non-existent transaction. SCARD_E_READER_UNAVAILABLE 0x80100017 The specified reader is not currently available for use. SCARD_P_SHUTDOWN 0x80100018 The operation has been aborted to allow the server application to exit. SCARD_E_PCI_TOO_SMALL 0x80100019 The PCI Receive buffer was too small. SCARD_E_READER_UNSUPPORTED 0x8010001A The reader driver does not meet minimal requirements for support. SCARD_E_DUPLICATE_READER 0x8010001B The reader driver did not produce a unique reader name. SCARD_E_CARD_UNSUPPORTED 0x8010001C The smart card does not meet minimal requirements for support. SCARD_E_NO_SERVICE 0x8010001D The Smart card resource manager is not running. SCARD_E_SERVICE_STOPPED 0x8010001E The Smart card resource manager has shut down. SCARD_E_UNEXPECTED 0x8010001F An unexpected card error has occurred. SCARD_E_ICC_INSTALLATION 0x80100020 No primary provider can be found for the smart card. SCARD_E_ICC_CREATEORDER 0x80100021 The requested order of object creation is not supported. SCARD_E_DIR_NOT_FOUND 0x80100023 The identified directory does not exist in the smart card. SCARD_E_FILE_NOT_FOUND 0x80100024 The identified file does not exist in the smart card. SCARD_E_NO_DIR 0x80100025 The supplied path does not represent a smart card directory. SCARD_E_NO_FILE 0x80100026 The supplied path does not represent a smart card file. SCARD_E_NO_ACCESS 0x80100027 Access is denied to this file. SCARD_E_WRITE_TOO_MANY 0x80100028 The smart card does not have enough memory to store the information. SCARD_E_BAD_SEEK 0x80100029 There was an error trying to set the smart card file object pointer. SCARD_E_INVALID_CHV 0x8010002A The supplied PIN is incorrect. SCARD_E_UNKNOWN_RES_MNG 0x8010002B An unrecognized error code was returned from a layered component. SCARD_E_NO_SUCH_CERTIFICATE 0x8010002C The requested certificate does not exist. SCARD_E_CERTIFICATE_UNAVAILABLE 0x8010002D The requested certificate could not be obtained. SCARD_E_NO_READERS_AVAILABLE 0x8010002E Cannot find a smart card reader. SCARD_E_COMM_DATA_LOST 0x8010002F A communications error with the smart card has been detected. More... SCARD_E_NO_KEY_CONTAINER 0x80100030 The requested key container does not exist on the smart card. SCARD_E_SERVER_TOO_BUSY 0x80100031 The Smart Card Resource Manager is too busy to complete this operation. SCARD_W_UNSUPPORTED_CARD 0x80100065 The reader cannot communicate with the card, due to ATR string configuration conflicts. SCARD_W_UNRESPONSIVE_CARD 0x80100066 The smart card is not responding to a reset. SCARD_W_UNPOWERED_CARD 0x80100067 Power has been removed from the smart card, so that further communication is not possible. SCARD_W_RESET_CARD 0x80100068 The smart card has been reset, so any shared state information is invalid. SCARD_W_REMOVED_CARD 0x80100069 The smart card has been removed, so further communication is not possible. SCARD_W_SECURITY_VIOLATION 0x8010006A Access was denied because of a security violation. SCARD_W_WRONG_CHV 0x8010006B The card cannot be accessed because the wrong PIN was presented. SCARD_W_CHV_BLOCKED 0x8010006C The card cannot be accessed because the maximum number of PIN entry attempts has been reached. SCARD_W_EOF 0x8010006D The end of the smart card file has been reached. SCARD_W_CANCELLED_BY_USER 0x8010006E The user pressed "Cancel" on a Smart Card Selection Dialog. SCARD_W_CARD_NOT_AUTHENTICATED 0x8010006F No PIN was presented to the smart card. piv-go-1.8.0/piv/pcsc_errors.go000066400000000000000000000120231407173563400164020ustar00rootroot00000000000000// Copyright 2020 Google LLC // // 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 // https://golang.org/s/generatedcode // Code generated by errors.py DO NOT EDIT. var pcscErrMsgs = map[int64]string{ 0x00000000: "no error was encountered", 0x80100001: "an internal consistency check failed", 0x80100002: "the action was cancelled by an SCardCancel request", 0x80100003: "the supplied handle was invalid", 0x80100004: "one or more of the supplied parameters could not be properly interpreted", 0x80100005: "registry startup information is missing or invalid", 0x80100006: "not enough memory available to complete this command", 0x80100007: "an internal consistency timer has expired", 0x80100008: "the data buffer to receive returned data is too small for the returned data", 0x80100009: "the specified reader name is not recognized", 0x8010000A: "the user-specified timeout value has expired", 0x8010000B: "the smart card cannot be accessed because of other connections outstanding", 0x8010000C: "the operation requires a Smart Card, but no Smart Card is currently in the device", 0x8010000D: "the specified smart card name is not recognized", 0x8010000E: "the system could not dispose of the media in the requested manner", 0x8010000F: "the requested protocols are incompatible with the protocol currently in use with the smart card", 0x80100010: "the reader or smart card is not ready to accept commands", 0x80100011: "one or more of the supplied parameters values could not be properly interpreted", 0x80100012: "the action was cancelled by the system, presumably to log off or shut down", 0x80100013: "an internal communications error has been detected", 0x80100014: "an internal error has been detected, but the source is unknown", 0x80100015: "an ATR obtained from the registry is not a valid ATR string", 0x80100016: "an attempt was made to end a non-existent transaction", 0x80100017: "the specified reader is not currently available for use", 0x80100018: "the operation has been aborted to allow the server application to exit", 0x80100019: "the PCI Receive buffer was too small", 0x8010001A: "the reader driver does not meet minimal requirements for support", 0x8010001B: "the reader driver did not produce a unique reader name", 0x8010001C: "the smart card does not meet minimal requirements for support", 0x8010001D: "the Smart card resource manager is not running", 0x8010001E: "the Smart card resource manager has shut down", 0x8010001F: "an unexpected card error has occurred", 0x80100020: "no primary provider can be found for the smart card", 0x80100021: "the requested order of object creation is not supported", 0x80100023: "the identified directory does not exist in the smart card", 0x80100024: "the identified file does not exist in the smart card", 0x80100025: "the supplied path does not represent a smart card directory", 0x80100026: "the supplied path does not represent a smart card file", 0x80100027: "access is denied to this file", 0x80100028: "the smart card does not have enough memory to store the information", 0x80100029: "there was an error trying to set the smart card file object pointer", 0x8010002A: "the supplied PIN is incorrect", 0x8010002B: "an unrecognized error code was returned from a layered component", 0x8010002C: "the requested certificate does not exist", 0x8010002D: "the requested certificate could not be obtained", 0x8010002E: "cannot find a smart card reader", 0x8010002F: "a communications error with the smart card has been detected. More..", 0x80100030: "the requested key container does not exist on the smart card", 0x80100031: "the Smart Card Resource Manager is too busy to complete this operation", 0x80100065: "the reader cannot communicate with the card, due to ATR string configuration conflicts", 0x80100066: "the smart card is not responding to a reset", 0x80100067: "power has been removed from the smart card, so that further communication is not possible", 0x80100068: "the smart card has been reset, so any shared state information is invalid", 0x80100069: "the smart card has been removed, so further communication is not possible", 0x8010006A: "access was denied because of a security violation", 0x8010006B: "the card cannot be accessed because the wrong PIN was presented", 0x8010006C: "the card cannot be accessed because the maximum number of PIN entry attempts has been reached", 0x8010006D: "the end of the smart card file has been reached", 0x8010006E: "the user pressed \"Cancel\" on a Smart Card Selection Dialog", 0x8010006F: "no PIN was presented to the smart card", } piv-go-1.8.0/piv/pcsc_errors.py000077500000000000000000000037131407173563400164360ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright 2020 Google LLC # # 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. with open("pcsc_errors") as f: data = f.read() name = "" val = 0 desc = "" with open("pcsc_errors.go", 'w+') as f: print("""// Copyright 2020 Google LLC // // 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 // https://golang.org/s/generatedcode // Code generated by errors.py DO NOT EDIT. var pcscErrMsgs = map[int64]string{""", file=f) for line in data.split("\n"): if not line.strip(): continue if line.startswith("SC"): name = line.split()[0][len("SCARD_E_"):] name = "".join([s[0] + s[1:].lower() for s in name.split("_")]) name = "rc" + name val = line.split()[1] else: desc = line[:-1] desc = desc[0].lower() + desc[1:] desc = desc.replace("\"", "\\\"") print("\t%s: \"%s\"," % (val, desc), file=f) print("}", file=f) piv-go-1.8.0/piv/pcsc_freebsd.go000066400000000000000000000015301407173563400165010ustar00rootroot00000000000000// Copyright 2020 Google LLC // // 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 "C" // Return codes for PCSC are different on different platforms (int vs. long). func scCheck(rc C.long) error { if rc == rcSuccess { return nil } return &scErr{int64(rc)} } func isRCNoReaders(rc C.long) bool { return rc == 0x8010002E } piv-go-1.8.0/piv/pcsc_linux.go000066400000000000000000000015411407173563400162300ustar00rootroot00000000000000// Copyright 2020 Google LLC // // 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 "C" // Return codes for PCSC are different on different platforms (int vs. long). func scCheck(rc C.long) error { if rc == rcSuccess { return nil } return &scErr{int64(rc)} } func isRCNoReaders(rc C.long) bool { return C.ulong(rc) == 0x8010002E } piv-go-1.8.0/piv/pcsc_test.go000066400000000000000000000072741407173563400160610ustar00rootroot00000000000000// Copyright 2020 Google LLC // // 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 ( "errors" "strings" "testing" ) func runContextTest(t *testing.T, f func(t *testing.T, c *scContext)) { ctx, err := newSCContext() if err != nil { t.Fatalf("creating context: %v", err) } defer func() { if err := ctx.Close(); err != nil { t.Errorf("closing context: %v", err) } }() f(t, ctx) } func TestContextClose(t *testing.T) { runContextTest(t, func(t *testing.T, c *scContext) {}) } func TestContextListReaders(t *testing.T) { runContextTest(t, testContextListReaders) } func testContextListReaders(t *testing.T, c *scContext) { if _, err := c.ListReaders(); err != nil { t.Errorf("listing readers: %v", err) } } func runHandleTest(t *testing.T, f func(t *testing.T, h *scHandle)) { runContextTest(t, func(t *testing.T, c *scContext) { readers, err := c.ListReaders() if err != nil { t.Fatalf("listing smartcard readers: %v", err) } reader := "" for _, r := range readers { if strings.Contains(strings.ToLower(r), "yubikey") { reader = r break } } if reader == "" { t.Skip("could not find yubikey, skipping testing") } h, err := c.Connect(reader) if err != nil { t.Fatalf("connecting to %s: %v", reader, err) } defer func() { if err := h.Close(); err != nil { t.Errorf("disconnecting from handle: %v", err) } }() f(t, h) }) } func TestHandle(t *testing.T) { runHandleTest(t, func(t *testing.T, h *scHandle) {}) } func TestTransaction(t *testing.T) { runHandleTest(t, func(t *testing.T, h *scHandle) { tx, err := h.Begin() if err != nil { t.Fatalf("beginning transaction: %v", err) } if err := tx.Close(); err != nil { t.Fatalf("closing transaction: %v", err) } }) } func TestErrors(t *testing.T) { var tests = []struct { sw1, sw2 byte isErrNotFound bool isAuthErr bool retries int desc string }{ {0x68, 0x82, false, false, 0, "secure messaging not supported"}, {0x63, 0x00, false, true, 0, "verification failed"}, {0x63, 0xc0, false, true, 0, "verification failed (0 retries remaining)"}, {0x63, 0xc1, false, true, 1, "verification failed (1 retry remaining)"}, {0x63, 0xcf, false, true, 15, "verification failed (15 retries remaining)"}, {0x63, 0x01, false, true, 1, "verification failed (1 retry remaining)"}, {0x63, 0x0f, false, true, 15, "verification failed (15 retries remaining)"}, {0x69, 0x83, false, true, 0, "authentication method blocked"}, {0x6a, 0x82, true, false, 0, "data object or application not found"}, } for _, tc := range tests { err := &apduErr{tc.sw1, tc.sw2} if errors.Is(err, ErrNotFound) != tc.isErrNotFound { var s string if !tc.isErrNotFound { s = " not" } t.Errorf("%q should%s be ErrNotFound", tc.desc, s) } var authErr AuthErr if errors.As(err, &authErr) != tc.isAuthErr { var s string if !tc.isAuthErr { s = " not" } t.Errorf("%q should%s be AuthErr", tc.desc, s) } if authErr.Retries != tc.retries { t.Errorf("%q retries should be %d, got %d", tc.desc, tc.retries, authErr.Retries) } if !strings.Contains(err.Error(), tc.desc) { t.Errorf("Error %v should contain text %v", err, tc.desc) } } } piv-go-1.8.0/piv/pcsc_unix.go000066400000000000000000000071351407173563400160610ustar00rootroot00000000000000// Copyright 2020 Google LLC // // 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. // +build darwin linux freebsd package piv // https://ludovicrousseau.blogspot.com/2010/04/pcsc-sample-in-c.html // #cgo darwin LDFLAGS: -framework PCSC // #cgo linux pkg-config: libpcsclite // #cgo freebsd CFLAGS: -I/usr/local/include/ // #cgo freebsd CFLAGS: -I/usr/local/include/PCSC // #cgo freebsd LDFLAGS: -L/usr/local/lib/ // #cgo freebsd LDFLAGS: -lpcsclite // #include // #include import "C" import ( "bytes" "fmt" "unsafe" ) const rcSuccess = C.SCARD_S_SUCCESS type scContext struct { ctx C.SCARDCONTEXT } func newSCContext() (*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 } func (c *scContext) Close() error { return scCheck(C.SCardReleaseContext(c.ctx)) } func (c *scContext) ListReaders() ([]string, error) { var n C.DWORD rc := C.SCardListReaders(c.ctx, nil, nil, &n) // On Linux, the PC/SC daemon will return an error when no smart cards are // available. Detect this and return nil with no smart cards instead. // // isRCNoReaders is defined in OS specific packages. if isRCNoReaders(rc) { return nil, nil } if err := scCheck(rc); err != nil { return nil, err } d := make([]byte, n) rc = C.SCardListReaders(c.ctx, nil, (*C.char)(unsafe.Pointer(&d[0])), &n) if err := scCheck(rc); err != nil { return nil, err } var readers []string for _, d := range bytes.Split(d, []byte{0}) { if len(d) > 0 { readers = append(readers, string(d)) } } return readers, nil } type scHandle struct { h C.SCARDHANDLE } func (c *scContext) Connect(reader string) (*scHandle, error) { var ( handle C.SCARDHANDLE activeProtocol C.DWORD ) rc := C.SCardConnect(c.ctx, C.CString(reader), C.SCARD_SHARE_EXCLUSIVE, C.SCARD_PROTOCOL_T1, &handle, &activeProtocol) if err := scCheck(rc); err != nil { return nil, err } return &scHandle{handle}, nil } func (h *scHandle) Close() error { return scCheck(C.SCardDisconnect(h.h, C.SCARD_LEAVE_CARD)) } type scTx struct { h C.SCARDHANDLE } func (h *scHandle) Begin() (*scTx, error) { if err := scCheck(C.SCardBeginTransaction(h.h)); err != nil { return nil, err } return &scTx{h.h}, nil } func (t *scTx) Close() error { return scCheck(C.SCardEndTransaction(t.h, C.SCARD_LEAVE_CARD)) } 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)) rc := C.SCardTransmit( t.h, C.SCARD_PCI_T1, (*C.BYTE)(&req[0]), reqN, nil, (*C.BYTE)(&resp[0]), &respN) if err := scCheck(rc); err != nil { return false, nil, fmt.Errorf("transmitting request: %w", err) } if respN < 2 { return false, nil, fmt.Errorf("scard response too short: %d", respN) } sw1 := resp[respN-2] sw2 := resp[respN-1] if sw1 == 0x90 && sw2 == 0x00 { return false, resp[:respN-2], nil } if sw1 == 0x61 { return true, resp[:respN-2], nil } return false, nil, &apduErr{sw1, sw2} } piv-go-1.8.0/piv/pcsc_windows.go000066400000000000000000000111151407173563400165610ustar00rootroot00000000000000// Copyright 2020 Google LLC // // 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 ( "fmt" "syscall" "unsafe" ) var ( winscard = syscall.NewLazyDLL("Winscard.dll") procSCardEstablishContext = winscard.NewProc("SCardEstablishContext") procSCardListReadersW = winscard.NewProc("SCardListReadersW") procSCardReleaseContext = winscard.NewProc("SCardReleaseContext") procSCardConnectW = winscard.NewProc("SCardConnectW") procSCardDisconnect = winscard.NewProc("SCardDisconnect") procSCardBeginTransaction = winscard.NewProc("SCardBeginTransaction") procSCardEndTransaction = winscard.NewProc("SCardEndTransaction") procSCardTransmit = winscard.NewProc("SCardTransmit") ) const ( scardScopeSystem = 2 scardShareExclusive = 1 scardLeaveCard = 0 scardProtocolT1 = 2 scardPCIT1 = 0 maxBufferSizeExtended = (4 + 3 + (1 << 16) + 3 + 2) rcSuccess = 0 ) func scCheck(rc uintptr) error { if rc == rcSuccess { return nil } return &scErr{int64(rc)} } func isRCNoReaders(rc uintptr) bool { return rc == 0x8010002E } type scContext struct { ctx syscall.Handle } func newSCContext() (*scContext, error) { var ctx syscall.Handle r0, _, _ := procSCardEstablishContext.Call( uintptr(scardScopeSystem), uintptr(0), uintptr(0), uintptr(unsafe.Pointer(&ctx)), ) if err := scCheck(r0); err != nil { return nil, err } return &scContext{ctx: ctx}, nil } func (c *scContext) Close() error { r0, _, _ := procSCardReleaseContext.Call(uintptr(c.ctx)) return scCheck(r0) } func (c *scContext) ListReaders() ([]string, error) { var n uint32 r0, _, _ := procSCardListReadersW.Call( uintptr(c.ctx), uintptr(unsafe.Pointer(nil)), uintptr(unsafe.Pointer(nil)), uintptr(unsafe.Pointer(&n)), ) if isRCNoReaders(r0) { return nil, nil } if err := scCheck(r0); err != nil { return nil, err } d := make([]uint16, n) r0, _, _ = procSCardListReadersW.Call( uintptr(c.ctx), uintptr(unsafe.Pointer(nil)), uintptr(unsafe.Pointer(&d[0])), uintptr(unsafe.Pointer(&n)), ) if err := scCheck(r0); err != nil { return nil, err } var readers []string j := 0 for i := 0; i < len(d); i++ { if d[i] != 0 { continue } readers = append(readers, syscall.UTF16ToString(d[j:i])) j = i + 1 if d[i+1] == 0 { break } } return readers, nil } func (c *scContext) Connect(reader string) (*scHandle, error) { var ( handle syscall.Handle activeProtocol uint16 ) r0, _, _ := procSCardConnectW.Call( uintptr(c.ctx), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(reader))), scardShareExclusive, scardProtocolT1, uintptr(unsafe.Pointer(&handle)), uintptr(activeProtocol), ) if err := scCheck(r0); err != nil { return nil, err } return &scHandle{handle}, nil } type scHandle struct { handle syscall.Handle } func (h *scHandle) Close() error { r0, _, _ := procSCardDisconnect.Call(uintptr(h.handle), scardLeaveCard) return scCheck(r0) } func (h *scHandle) Begin() (*scTx, error) { r0, _, _ := procSCardBeginTransaction.Call(uintptr(h.handle)) if err := scCheck(r0); err != nil { return nil, err } return &scTx{h.handle}, nil } func (t *scTx) Close() error { r0, _, _ := procSCardEndTransaction.Call(uintptr(t.handle), scardLeaveCard) return scCheck(r0) } type scTx struct { handle syscall.Handle } func (t *scTx) transmit(req []byte) (more bool, b []byte, err error) { var resp [maxBufferSizeExtended]byte reqN := len(req) respN := len(resp) r0, _, _ := procSCardTransmit.Call( uintptr(t.handle), uintptr(scardPCIT1), uintptr(unsafe.Pointer(&req[0])), uintptr(reqN), uintptr(0), uintptr(unsafe.Pointer(&resp[0])), uintptr(unsafe.Pointer(&respN)), ) if err := scCheck(r0); err != nil { return false, nil, fmt.Errorf("transmitting request: %w", err) } if respN < 2 { return false, nil, fmt.Errorf("scard response too short: %d", respN) } sw1 := resp[respN-2] sw2 := resp[respN-1] if sw1 == 0x90 && sw2 == 0x00 { return false, resp[:respN-2], nil } if sw1 == 0x61 { return true, resp[:respN-2], nil } return false, nil, &apduErr{sw1, sw2} } piv-go-1.8.0/piv/piv.go000066400000000000000000000545531407173563400146720ustar00rootroot00000000000000// Copyright 2020 Google LLC // // 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 ( "bytes" "crypto/des" "crypto/rand" "encoding/asn1" "encoding/binary" "errors" "fmt" "io" "math/big" ) var ( // DefaultPIN for the PIV applet. The PIN is used to change the Management Key, // and slots can optionally require it to perform signing operations. DefaultPIN = "123456" // DefaultPUK for the PIV applet. The PUK is only used to reset the PIN when // the card's PIN retries have been exhausted. DefaultPUK = "12345678" // DefaultManagementKey for the PIV applet. The Management Key is a Triple-DES // key required for slot actions such as generating keys, setting certificates, // and signing. DefaultManagementKey = [24]byte{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, } ) // Cards lists all smart cards available via PC/SC interface. Card names are // strings describing the key, such as "Yubico Yubikey NEO OTP+U2F+CCID 00 00". // // Card names depend on the operating system and what port a card is plugged // into. To uniquely identify a card, use its serial number. // // See: https://ludovicrousseau.blogspot.com/2010/05/what-is-in-pcsc-reader-name.html func Cards() ([]string, error) { var c client return c.Cards() } const ( // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-78-4.pdf#page=17 algTag = 0x80 alg3DES = 0x03 algRSA1024 = 0x06 algRSA2048 = 0x07 algECCP256 = 0x11 algECCP384 = 0x14 // non-standard; as implemented by SoloKeys. Chosen for low probability of eventual // clashes, if and when PIV standard adds Ed25519 support algEd25519 = 0x22 // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-78-4.pdf#page=16 keyAuthentication = 0x9a keyCardManagement = 0x9b keySignature = 0x9c keyKeyManagement = 0x9d keyCardAuthentication = 0x9e keyAttestation = 0xf9 insVerify = 0x20 insChangeReference = 0x24 insResetRetry = 0x2c insGenerateAsymmetric = 0x47 insAuthenticate = 0x87 insGetData = 0xcb insPutData = 0xdb insSelectApplication = 0xa4 insGetResponseAPDU = 0xc0 // https://github.com/Yubico/yubico-piv-tool/blob/yubico-piv-tool-1.7.0/lib/ykpiv.h#L656 insSetMGMKey = 0xff insImportKey = 0xfe insGetVersion = 0xfd insReset = 0xfb insSetPINRetries = 0xfa insAttest = 0xf9 insGetSerial = 0xf8 ) // YubiKey is an exclusive open connection to a YubiKey smart card. While open, // no other process can query the given card. // // To release the connection, call the Close method. type YubiKey struct { ctx *scContext h *scHandle tx *scTx rand io.Reader // Used to determine how to access certain functionality. // // TODO: It's not clear what this actually communicates. Is this the // YubiKey's version or PIV version? A NEO reports v1.0.4. Figure this out // before exposing an API. version *version } // Close releases the connection to the smart card. func (yk *YubiKey) Close() error { err1 := yk.h.Close() err2 := yk.ctx.Close() if err1 == nil { return err2 } return err1 } // Open connects to a YubiKey smart card. func Open(card string) (*YubiKey, error) { var c client return c.Open(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 { // Rand is a cryptographic source of randomness used for card challenges. // // If nil, defaults to crypto.Rand. Rand io.Reader } func (c *client) Cards() ([]string, error) { ctx, err := newSCContext() if err != nil { return nil, fmt.Errorf("connecting to pscs: %w", err) } defer ctx.Close() return ctx.ListReaders() } func (c *client) Open(card string) (*YubiKey, error) { ctx, err := newSCContext() if err != nil { return nil, fmt.Errorf("connecting to smart card daemon: %w", err) } h, err := ctx.Connect(card) if err != nil { ctx.Close() return nil, fmt.Errorf("connecting to smart card: %w", err) } tx, err := h.Begin() 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) } yk := &YubiKey{ctx: ctx, h: h, tx: tx} v, err := ykVersion(yk.tx) if err != nil { yk.Close() return nil, fmt.Errorf("getting yubikey version: %w", err) } yk.version = v if c.Rand != nil { yk.rand = c.Rand } else { yk.rand = rand.Reader } return yk, nil } // Version returns the version as reported by the PIV applet. For newer // YubiKeys (>=4.0.0) this corresponds to the version of the YubiKey itself. // // Older YubiKeys return values that aren't directly related to the YubiKey // version. For example, 3rd generation YubiKeys report 1.0.X. func (yk *YubiKey) Version() Version { return Version{ Major: int(yk.version.major), Minor: int(yk.version.minor), Patch: int(yk.version.patch), } } // Serial returns the YubiKey's serial number. func (yk *YubiKey) Serial() (uint32, error) { return ykSerial(yk.tx, yk.version) } func encodePIN(pin string) ([]byte, error) { data := []byte(pin) if len(data) == 0 { return nil, fmt.Errorf("pin cannot be empty") } if len(data) > 8 { return nil, fmt.Errorf("pin longer than 8 bytes") } // apply padding for i := len(data); i < 8; i++ { data = append(data, 0xff) } return data, nil } // authPIN attempts to authenticate against the card with the provided PIN. // The PIN is required to use and modify certain slots. // // After a specific number of authentication attemps with an invalid PIN, // usually 3, the PIN will become block and refuse further attempts. At that // point the PUK must be used to unblock the PIN. // // Use DefaultPIN if the PIN hasn't been set. func (yk *YubiKey) authPIN(pin string) error { return ykLogin(yk.tx, pin) } func ykLogin(tx *scTx, pin string) error { data, err := encodePIN(pin) if err != nil { return err } // https://csrc.nist.gov/CSRC/media/Publications/sp/800-73/4/archive/2015-05-29/documents/sp800_73-4_pt2_draft.pdf#page=20 cmd := apdu{instruction: insVerify, param2: 0x80, data: data} if _, err := tx.Transmit(cmd); err != nil { return fmt.Errorf("verify pin: %w", err) } return nil } func ykLoginNeeded(tx *scTx) bool { cmd := apdu{instruction: insVerify, param2: 0x80} _, err := tx.Transmit(cmd) return err != nil } // Retries returns the number of attempts remaining to enter the correct PIN. func (yk *YubiKey) Retries() (int, error) { return ykPINRetries(yk.tx) } func ykPINRetries(tx *scTx) (int, error) { cmd := apdu{instruction: insVerify, param2: 0x80} _, err := tx.Transmit(cmd) if err == nil { return 0, fmt.Errorf("expected error code from empty pin") } var e AuthErr if errors.As(err, &e) { return e.Retries, nil } return 0, fmt.Errorf("invalid response: %w", err) } // Reset resets the YubiKey PIV applet to its factory settings, wiping all slots // and resetting the PIN, PUK, and Management Key to their default values. This // does NOT affect data on other applets, such as GPG or U2F. func (yk *YubiKey) Reset() error { return ykReset(yk.tx, yk.rand) } func ykReset(tx *scTx, r io.Reader) error { // Reset only works if both the PIN and PUK are blocked. Before resetting, // try the wrong PIN and PUK multiple times to block them. maxPIN := big.NewInt(100_000_000) pinInt, err := rand.Int(r, maxPIN) if err != nil { return fmt.Errorf("generating random pin: %v", err) } pukInt, err := rand.Int(r, maxPIN) if err != nil { return fmt.Errorf("generating random puk: %v", err) } pin := pinInt.String() puk := pukInt.String() for { err := ykLogin(tx, pin) if err == nil { // TODO: do we care about a 1/100million chance? return fmt.Errorf("expected error with random pin") } var e AuthErr if !errors.As(err, &e) { return fmt.Errorf("blocking pin: %w", err) } if e.Retries == 0 { break } } for { err := ykChangePUK(tx, puk, puk) if err == nil { // TODO: do we care about a 1/100million chance? return fmt.Errorf("expected error with random puk") } var e AuthErr if !errors.As(err, &e) { return fmt.Errorf("blocking puk: %w", err) } if e.Retries == 0 { break } } cmd := apdu{instruction: insReset} if _, err := tx.Transmit(cmd); err != nil { return fmt.Errorf("reseting yubikey: %w", err) } return nil } type version struct { major byte minor byte patch byte } // authManagementKey attempts to authenticate against the card with the provided // management key. The management key is required to generate new keys or add // certificates to slots. // // Use DefaultManagementKey if the management key hasn't been set. func (yk *YubiKey) authManagementKey(key [24]byte) error { return ykAuthenticate(yk.tx, key, yk.rand) } var ( // Smartcard Application IDs for YubiKeys. // // https://github.com/Yubico/yubico-piv-tool/blob/yubico-piv-tool-1.7.0/lib/ykpiv.c#L1877 // https://github.com/Yubico/yubico-piv-tool/blob/yubico-piv-tool-1.7.0/lib/ykpiv.c#L108-L110 // https://github.com/Yubico/yubico-piv-tool/blob/yubico-piv-tool-1.7.0/lib/ykpiv.c#L1117 aidManagement = [...]byte{0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17} aidPIV = [...]byte{0xa0, 0x00, 0x00, 0x03, 0x08} aidYubiKey = [...]byte{0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01} ) func ykAuthenticate(tx *scTx, key [24]byte, rand io.Reader) error { // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=92 // https://tsapps.nist.gov/publication/get_pdf.cfm?pub_id=918402#page=114 // request a witness cmd := apdu{ instruction: insAuthenticate, param1: alg3DES, param2: keyCardManagement, data: []byte{ 0x7c, // Dynamic Authentication Template tag 0x02, // Length of object 0x80, // 'Witness' 0x00, // Return encrypted random }, } resp, err := tx.Transmit(cmd) if err != nil { return fmt.Errorf("get auth challenge: %w", err) } if n := len(resp); n < 12 { return fmt.Errorf("challenge didn't return enough bytes: %d", n) } if !bytes.Equal(resp[:4], []byte{ 0x7c, 0x0a, 0x80, // 'Witness' 0x08, // Tag length }) { return fmt.Errorf("invalid authentication object header: %x", resp[:4]) } cardChallenge := resp[4 : 4+8] cardResponse := make([]byte, 8) block, err := des.NewTripleDESCipher(key[:]) if err != nil { return fmt.Errorf("creating triple des block cipher: %v", err) } block.Decrypt(cardResponse, cardChallenge) challenge := make([]byte, 8) if _, err := io.ReadFull(rand, challenge); err != nil { return fmt.Errorf("reading rand data: %v", err) } response := make([]byte, 8) block.Encrypt(response, challenge) data := append([]byte{ 0x7c, // Dynamic Authentication Template tag 20, // 2+8+2+8 0x80, // 'Witness' 0x08, // Tag length }) data = append(data, cardResponse...) data = append(data, 0x81, // 'Challenge' 0x08, // Tag length ) data = append(data, challenge...) cmd = apdu{ instruction: insAuthenticate, param1: alg3DES, param2: keyCardManagement, data: data, } resp, err = tx.Transmit(cmd) if err != nil { return fmt.Errorf("auth challenge: %w", err) } if n := len(resp); n < 12 { return fmt.Errorf("challenge response didn't return enough bytes: %d", n) } if !bytes.Equal(resp[:4], []byte{ 0x7c, 0x0a, 0x82, // 'Response' 0x08, }) { return fmt.Errorf("response invalid authentication object header: %x", resp[:4]) } if !bytes.Equal(resp[4:4+8], response) { return fmt.Errorf("challenge failed") } return nil } // SetManagementKey updates the management key to a new key. Management keys // are triple-des keys, however padding isn't verified. To generate a new key, // generate 24 random bytes. // // var newKey [24]byte // if _, err := io.ReadFull(rand.Reader, newKey[:]); err != nil { // // ... // } // if err := yk.SetManagementKey(piv.DefaultManagementKey, newKey); err != nil { // // ... // } // // func (yk *YubiKey) SetManagementKey(oldKey, newKey [24]byte) error { if err := ykAuthenticate(yk.tx, oldKey, yk.rand); err != nil { return fmt.Errorf("authenticating with old key: %w", err) } if err := ykSetManagementKey(yk.tx, newKey, false); err != nil { return err } return nil } // ykSetManagementKey updates the management key to a new key. This requires // authenticating with the existing management key. func ykSetManagementKey(tx *scTx, key [24]byte, touch bool) error { cmd := apdu{ instruction: insSetMGMKey, param1: 0xff, param2: 0xff, data: append([]byte{ alg3DES, keyCardManagement, 24, }, key[:]...), } if touch { cmd.param2 = 0xfe } if _, err := tx.Transmit(cmd); err != nil { return fmt.Errorf("command failed: %w", err) } return nil } // SetPIN updates the PIN to a new value. For compatibility, PINs should be 1-8 // numeric characters. // // To generate a new PIN, use the crypto/rand package. // // // Generate a 6 character PIN. // newPINInt, err := rand.Int(rand.Reader, bit.NewInt(1_000_000)) // if err != nil { // // ... // } // // Format with leading zeros. // newPIN := fmt.Sprintf("%06d", newPINInt) // if err := yk.SetPIN(piv.DefaultPIN, newPIN); err != nil { // // ... // } // func (yk *YubiKey) SetPIN(oldPIN, newPIN string) error { return ykChangePIN(yk.tx, oldPIN, newPIN) } func ykChangePIN(tx *scTx, oldPIN, newPIN string) error { oldPINData, err := encodePIN(oldPIN) if err != nil { return fmt.Errorf("encoding old pin: %v", err) } newPINData, err := encodePIN(newPIN) if err != nil { return fmt.Errorf("encoding new pin: %v", err) } cmd := apdu{ instruction: insChangeReference, param2: 0x80, data: append(oldPINData, newPINData...), } _, err = tx.Transmit(cmd) return err } // Unblock unblocks the PIN, setting it to a new value. func (yk *YubiKey) Unblock(puk, newPIN string) error { return ykUnblockPIN(yk.tx, puk, newPIN) } func ykUnblockPIN(tx *scTx, puk, newPIN string) error { pukData, err := encodePIN(puk) if err != nil { return fmt.Errorf("encoding puk: %v", err) } newPINData, err := encodePIN(newPIN) if err != nil { return fmt.Errorf("encoding new pin: %v", err) } cmd := apdu{ instruction: insResetRetry, param2: 0x80, data: append(pukData, newPINData...), } _, err = tx.Transmit(cmd) return err } // SetPUK updates the PUK to a new value. For compatibility, PUKs should be 1-8 // numeric characters. // // To generate a new PUK, use the crypto/rand package. // // // Generate a 8 character PUK. // newPUKInt, err := rand.Int(rand.Reader, bit.NewInt(100_000_000)) // if err != nil { // // ... // } // // Format with leading zeros. // newPUK := fmt.Sprintf("%08d", newPUKInt) // if err := yk.SetPIN(piv.DefaultPUK, newPUK); err != nil { // // ... // } // func (yk *YubiKey) SetPUK(oldPUK, newPUK string) error { return ykChangePUK(yk.tx, oldPUK, newPUK) } func ykChangePUK(tx *scTx, oldPUK, newPUK string) error { oldPUKData, err := encodePIN(oldPUK) if err != nil { return fmt.Errorf("encoding old puk: %v", err) } newPUKData, err := encodePIN(newPUK) if err != nil { return fmt.Errorf("encoding new puk: %v", err) } cmd := apdu{ instruction: insChangeReference, param2: 0x81, data: append(oldPUKData, newPUKData...), } _, err = tx.Transmit(cmd) return err } func ykSelectApplication(tx *scTx, id []byte) error { cmd := apdu{ instruction: insSelectApplication, param1: 0x04, data: id[:], } if _, err := tx.Transmit(cmd); err != nil { return fmt.Errorf("command failed: %w", err) } return nil } func ykVersion(tx *scTx) (*version, error) { cmd := apdu{ instruction: insGetVersion, } resp, err := tx.Transmit(cmd) if err != nil { return nil, fmt.Errorf("command failed: %w", err) } if n := len(resp); n != 3 { return nil, fmt.Errorf("expected response to have 3 bytes, got: %d", n) } return &version{resp[0], resp[1], resp[2]}, nil } func ykSerial(tx *scTx, v *version) (uint32, error) { cmd := apdu{instruction: insGetSerial} if v.major < 5 { // Earlier versions of YubiKeys required using the yubikey applet to get // the serial number. Newer ones have this built into the PIV applet. if err := ykSelectApplication(tx, aidYubiKey[:]); err != nil { return 0, fmt.Errorf("selecting yubikey applet: %w", err) } defer ykSelectApplication(tx, aidPIV[:]) cmd = apdu{instruction: 0x01, param1: 0x10} } resp, err := tx.Transmit(cmd) if err != nil { return 0, fmt.Errorf("smart card command: %w", err) } if n := len(resp); n != 4 { return 0, fmt.Errorf("expected 4 byte serial number, got %d", n) } return binary.BigEndian.Uint32(resp), nil } // ykChangeManagementKey sets the Management Key to the new key provided. The // user must have authenticated with the existing key first. func ykChangeManagementKey(tx *scTx, key [24]byte) error { cmd := apdu{ instruction: insSetMGMKey, param1: 0xff, param2: 0xff, // TODO: support touch policy data: append([]byte{ alg3DES, keyCardManagement, 24, }, key[:]...), } if _, err := tx.Transmit(cmd); err != nil { return fmt.Errorf("command failed: %w", err) } return nil } func unmarshalDERField(b []byte, tag uint64) (obj []byte, err error) { var prefix []byte for tag > 0 { prefix = append(prefix, byte(tag)) tag = tag >> 8 } for i, j := 0, len(prefix)-1; i < j; i, j = i+1, j-1 { prefix[i], prefix[j] = prefix[j], prefix[i] } hasPrefix := bytes.HasPrefix(b, prefix) for len(b) > 0 { var v asn1.RawValue b, err = asn1.Unmarshal(b, &v) if err != nil { return nil, err } if hasPrefix { return v.Bytes, nil } } return nil, fmt.Errorf("no der value with tag 0x%x", prefix) } // Metadata returns protected data stored on the card. This can be used to // retrieve PIN protected management keys. func (yk *YubiKey) Metadata(pin string) (*Metadata, error) { m, err := ykGetProtectedMetadata(yk.tx, pin) if err != nil { if errors.Is(err, ErrNotFound) { return &Metadata{}, nil } return nil, err } return m, nil } // SetMetadata sets PIN protected metadata on the key. This is primarily to // store the management key on the smart card instead of managing the PIN and // management key seperately. func (yk *YubiKey) SetMetadata(key [24]byte, m *Metadata) error { return ykSetProtectedMetadata(yk.tx, key, m) } // Metadata holds protected metadata. This is primarily used by YubiKey manager // to implement PIN protect management keys, storing management keys on the card // guarded by the PIN. type Metadata struct { // ManagementKey is the management key stored directly on the YubiKey. ManagementKey *[24]byte // raw, if not nil, is the full bytes raw []byte } func (m *Metadata) marshal() ([]byte, error) { if m.raw == nil { if m.ManagementKey == nil { return []byte{0x88, 0x00}, nil } return append([]byte{ 0x88, 26, 0x89, 24, }, m.ManagementKey[:]...), nil } if m.ManagementKey == nil { return m.raw, nil } var metadata asn1.RawValue if _, err := asn1.Unmarshal(m.raw, &metadata); err != nil { return nil, fmt.Errorf("updating metadata: %v", err) } if !bytes.HasPrefix(metadata.FullBytes, []byte{0x88}) { return nil, fmt.Errorf("expected tag: 0x88") } raw := metadata.Bytes metadata.Bytes = nil metadata.FullBytes = nil for len(raw) > 0 { var ( err error v asn1.RawValue ) raw, err = asn1.Unmarshal(raw, &v) if err != nil { return nil, fmt.Errorf("unmarshal metadata field: %v", err) } if bytes.HasPrefix(v.FullBytes, []byte{0x89}) { continue } metadata.Bytes = append(metadata.Bytes, v.FullBytes...) } metadata.Bytes = append(metadata.Bytes, 0x89, 24) metadata.Bytes = append(metadata.Bytes, m.ManagementKey[:]...) return asn1.Marshal(metadata) } func (m *Metadata) unmarshal(b []byte) error { m.raw = b var md asn1.RawValue if _, err := asn1.Unmarshal(b, &md); err != nil { return err } if !bytes.HasPrefix(md.FullBytes, []byte{0x88}) { return fmt.Errorf("expected tag: 0x88") } d := md.Bytes for len(d) > 0 { var ( err error v asn1.RawValue ) d, err = asn1.Unmarshal(d, &v) if err != nil { return fmt.Errorf("unmarshal metadata field: %v", err) } if !bytes.HasPrefix(v.FullBytes, []byte{0x89}) { continue } // 0x89 indicates key if len(v.Bytes) != 24 { return fmt.Errorf("invalid management key length: %d", len(v.Bytes)) } var key [24]byte for i := 0; i < len(v.Bytes); i++ { key[i] = v.Bytes[i] } m.ManagementKey = &key } return nil } func ykGetProtectedMetadata(tx *scTx, pin string) (*Metadata, error) { // NOTE: for some reason this action requires the PIN to be authenticated on // the same transaction. It doesn't work otherwise. if err := ykLogin(tx, pin); err != nil { return nil, fmt.Errorf("authenticating with pin: %w", err) } cmd := apdu{ instruction: insGetData, param1: 0x3f, param2: 0xff, data: []byte{ 0x5c, // Tag list 0x03, 0x5f, 0xc1, 0x09, }, } resp, err := tx.Transmit(cmd) if err != nil { return nil, fmt.Errorf("command failed: %w", err) } obj, _, err := unmarshalASN1(resp, 1, 0x13) // tag 0x53 if err != nil { return nil, fmt.Errorf("unmarshaling response: %v", err) } var m Metadata if err := m.unmarshal(obj); err != nil { return nil, fmt.Errorf("unmarshal protected metadata: %v", err) } return &m, nil } func ykSetProtectedMetadata(tx *scTx, key [24]byte, m *Metadata) error { data, err := m.marshal() if err != nil { return fmt.Errorf("encoding metadata: %v", err) } data = append([]byte{ 0x5c, // Tag list 0x03, 0x5f, 0xc1, 0x09, }, marshalASN1(0x53, data)...) cmd := apdu{ instruction: insPutData, param1: 0x3f, param2: 0xff, data: data, } // NOTE: for some reason this action requires the management key authenticated // on the same transaction. It doesn't work otherwise. if err := ykAuthenticate(tx, key, rand.Reader); err != nil { return fmt.Errorf("authenticating with key: %w", err) } if _, err := tx.Transmit(cmd); err != nil { return fmt.Errorf("command failed: %w", err) } return nil } piv-go-1.8.0/piv/piv_test.go000066400000000000000000000264731407173563400157310ustar00rootroot00000000000000// Copyright 2020 Google LLC // // 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 ( "bytes" "crypto/rand" "encoding/hex" "errors" "flag" "io" "math/bits" "strings" "testing" ) // canModifyYubiKey indicates whether the test running has constented to // destroying data on YubiKeys connected to the system. var canModifyYubiKey bool func init() { flag.BoolVar(&canModifyYubiKey, "wipe-yubikey", false, "Flag required to run tests that access the yubikey") } func testGetVersion(t *testing.T, h *scHandle) { tx, err := h.Begin() if err != nil { t.Fatalf("new transaction: %v", err) } defer tx.Close() if err := ykSelectApplication(tx, aidPIV[:]); err != nil { t.Fatalf("selecting application: %v", err) } if _, err := ykVersion(tx); err != nil { t.Fatalf("listing version: %v", err) } } func supportsVersion(v Version, major, minor, patch int) bool { if v.Major != major { return v.Major > major } if v.Minor != minor { return v.Minor > minor } return v.Patch >= patch } func testRequiresVersion(t *testing.T, yk *YubiKey, major, minor, patch int) { v := yk.Version() if !supportsVersion(v, major, minor, patch) { t.Skipf("test requires yubikey version %d.%d.%d: got %d.%d.%d", major, minor, patch, v.Major, v.Minor, v.Patch) } } func TestGetVersion(t *testing.T) { runHandleTest(t, testGetVersion) } func TestCards(t *testing.T) { if _, err := Cards(); err != nil { t.Fatalf("listing cards: %v", err) } } func newTestYubiKey(t *testing.T) (*YubiKey, func()) { cards, err := Cards() if err != nil { t.Fatalf("listing cards: %v", err) } for _, card := range cards { if !strings.Contains(strings.ToLower(card), "yubikey") { continue } if !canModifyYubiKey { t.Skip("not running test that accesses yubikey, provide --wipe-yubikey flag") } yk, err := Open(card) if err != nil { t.Fatalf("getting new yubikey: %v", err) } return yk, func() { if err := yk.Close(); err != nil { t.Errorf("closing yubikey: %v", err) } } } t.Skip("no yubikeys detected, skipping") return nil, nil } func TestNewYubiKey(t *testing.T) { _, close := newTestYubiKey(t) defer close() } func TestMultipleConnections(t *testing.T) { cards, err := Cards() if err != nil { t.Fatalf("listing cards: %v", err) } for _, card := range cards { if !strings.Contains(strings.ToLower(card), "yubikey") { continue } if !canModifyYubiKey { t.Skip("not running test that accesses yubikey, provide --wipe-yubikey flag") } yk, err := Open(card) if err != nil { t.Fatalf("getting new yubikey: %v", err) } defer func() { if err := yk.Close(); err != nil { t.Errorf("closing yubikey: %v", err) } }() _, oerr := Open(card) if oerr == nil { t.Fatalf("expected second open operation to fail") } var e *scErr if !errors.As(oerr, &e) { t.Fatalf("expected scErr, got %v", oerr) } if e.rc != 0x8010000B { t.Fatalf("expected return code 0x8010000B, got 0x%x", e.rc) } return } t.Skip("no yubikeys detected, skipping") } func TestYubiKeySerial(t *testing.T) { yk, close := newTestYubiKey(t) defer close() if _, err := yk.Serial(); err != nil { t.Fatalf("getting serial number: %v", err) } } func TestYubiKeyLoginNeeded(t *testing.T) { yk, close := newTestYubiKey(t) defer close() testRequiresVersion(t, yk, 4, 3, 0) if !ykLoginNeeded(yk.tx) { t.Errorf("expected login needed") } if err := ykLogin(yk.tx, DefaultPIN); err != nil { t.Fatalf("login: %v", err) } if ykLoginNeeded(yk.tx) { t.Errorf("expected no login needed") } } func TestYubiKeyPINRetries(t *testing.T) { yk, close := newTestYubiKey(t) defer close() retries, err := yk.Retries() if err != nil { t.Fatalf("getting retries: %v", err) } if retries < 0 || retries > 15 { t.Fatalf("invalid number of retries: %d", retries) } } func TestYubiKeyReset(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.authPIN(DefaultPIN); err != nil { t.Fatalf("login: %v", err) } } func TestYubiKeyLogin(t *testing.T) { yk, close := newTestYubiKey(t) defer close() if err := yk.authPIN(DefaultPIN); err != nil { t.Fatalf("login: %v", err) } } func TestYubiKeyAuthenticate(t *testing.T) { yk, close := newTestYubiKey(t) defer close() if err := yk.authManagementKey(DefaultManagementKey); err != nil { t.Errorf("authenticating: %v", err) } } func TestYubiKeySetManagementKey(t *testing.T) { yk, close := newTestYubiKey(t) defer close() var mgmtKey [24]byte if _, err := io.ReadFull(rand.Reader, mgmtKey[:]); err != nil { t.Fatalf("generating management key: %v", err) } if err := yk.SetManagementKey(DefaultManagementKey, mgmtKey); err != nil { t.Fatalf("setting management key: %v", err) } if err := yk.authManagementKey(mgmtKey); err != nil { t.Errorf("authenticating with new management key: %v", err) } if err := yk.SetManagementKey(mgmtKey, DefaultManagementKey); err != nil { t.Fatalf("resetting management key: %v", err) } } func TestYubiKeyUnblockPIN(t *testing.T) { yk, close := newTestYubiKey(t) defer close() badPIN := "0" for { err := ykLogin(yk.tx, badPIN) if err == nil { t.Fatalf("login with bad pin succeeded") } var e AuthErr if !errors.As(err, &e) { t.Fatalf("error returned was not a wrong pin error: %v", err) } if e.Retries == 0 { break } } if err := yk.Unblock(DefaultPUK, DefaultPIN); err != nil { t.Fatalf("unblocking pin: %v", err) } if err := ykLogin(yk.tx, DefaultPIN); err != nil { t.Errorf("failed to login with pin after unblock: %v", err) } } func TestYubiKeyChangePIN(t *testing.T) { yk, close := newTestYubiKey(t) defer close() newPIN := "654321" if err := yk.SetPIN(newPIN, newPIN); err == nil { t.Errorf("successfully changed pin with invalid pin, expected error") } if err := yk.SetPIN(DefaultPIN, newPIN); err != nil { t.Fatalf("changing pin: %v", err) } if err := yk.SetPIN(newPIN, DefaultPIN); err != nil { t.Fatalf("resetting pin: %v", err) } } func TestYubiKeyChangePUK(t *testing.T) { yk, close := newTestYubiKey(t) defer close() newPUK := "87654321" if err := yk.SetPUK(newPUK, newPUK); err == nil { t.Errorf("successfully changed puk with invalid puk, expected error") } if err := yk.SetPUK(DefaultPUK, newPUK); err != nil { t.Fatalf("changing puk: %v", err) } if err := yk.SetPUK(newPUK, DefaultPUK); err != nil { t.Fatalf("resetting puk: %v", err) } } func TestChangeManagementKey(t *testing.T) { yk, close := newTestYubiKey(t) defer close() var newKey [24]byte if _, err := io.ReadFull(rand.Reader, newKey[:]); err != nil { t.Fatalf("generating new management key: %v", err) } // Apply odd-parity for i, b := range newKey { if bits.OnesCount8(uint8(b))%2 == 0 { newKey[i] = b ^ 1 // flip least significant bit } } if err := yk.SetManagementKey(newKey, newKey); err == nil { t.Errorf("successfully changed management key with invalid key, expected error") } if err := yk.SetManagementKey(DefaultManagementKey, newKey); err != nil { t.Fatalf("changing management key: %v", err) } if err := yk.SetManagementKey(newKey, DefaultManagementKey); err != nil { t.Fatalf("resetting management key: %v", err) } } func TestMetadata(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } func() { yk, close := newTestYubiKey(t) defer close() if err := yk.Reset(); err != nil { t.Fatalf("resetting yubikey: %v", err) } }() yk, close := newTestYubiKey(t) defer close() if m, err := yk.Metadata(DefaultPIN); err != nil { t.Errorf("getting metadata: %v", err) } else if m.ManagementKey != nil { t.Errorf("expected no management key set") } wantKey := [24]byte{ 0x09, 0xd9, 0x87, 0x81, 0xfb, 0xdc, 0xc9, 0xb6, 0x91, 0xa2, 0x05, 0x80, 0x6e, 0xc0, 0xba, 0x84, 0x31, 0xac, 0x0d, 0x9f, 0x59, 0xa5, 0x00, 0xad, } m := &Metadata{ ManagementKey: &wantKey, } if err := yk.SetMetadata(DefaultManagementKey, m); err != nil { t.Fatalf("setting metadata: %v", err) } got, err := yk.Metadata(DefaultPIN) if err != nil { t.Fatalf("getting metadata: %v", err) } if got.ManagementKey == nil { t.Errorf("no management key") } else if *got.ManagementKey != wantKey { t.Errorf("wanted management key=0x%x, got=0x%x", wantKey, got.ManagementKey) } } func TestMetadataUnmarshal(t *testing.T) { data, _ := hex.DecodeString("881a891809d98781fbdcc9b691a205806ec0ba8431ac0d9f59a500ad") wantKey := [24]byte{ 0x09, 0xd9, 0x87, 0x81, 0xfb, 0xdc, 0xc9, 0xb6, 0x91, 0xa2, 0x05, 0x80, 0x6e, 0xc0, 0xba, 0x84, 0x31, 0xac, 0x0d, 0x9f, 0x59, 0xa5, 0x00, 0xad, } var m Metadata if err := m.unmarshal(data); err != nil { t.Fatalf("parsing metadata: %v", err) } if m.ManagementKey == nil { t.Fatalf("no management key") } gotKey := *m.ManagementKey if gotKey != wantKey { t.Errorf("(*Metadata).unmarshal, got key=0x%x, want key=0x%x", gotKey, wantKey) } } func TestMetadataMarshal(t *testing.T) { key := [24]byte{ 0x09, 0xd9, 0x87, 0x81, 0xfb, 0xdc, 0xc9, 0xb6, 0x91, 0xa2, 0x05, 0x80, 0x6e, 0xc0, 0xba, 0x84, 0x31, 0xac, 0x0d, 0x9f, 0x59, 0xa5, 0x00, 0xad, } want := append([]byte{ 0x88, 26, 0x89, 24, }, key[:]...) m := Metadata{ ManagementKey: &key, } got, err := m.marshal() if err != nil { t.Fatalf("marshaling key: %v", err) } if !bytes.Equal(want, got) { t.Errorf("(*Metadata.marshal, got=0x%x, want=0x%x", got, want) } } func TestMetadataUpdate(t *testing.T) { key := [24]byte{ 0x09, 0xd9, 0x87, 0x81, 0xfb, 0xdc, 0xc9, 0xb6, 0x91, 0xa2, 0x05, 0x80, 0x6e, 0xc0, 0xba, 0x84, 0x31, 0xac, 0x0d, 0x9f, 0x59, 0xa5, 0x00, 0xad, } want := append([]byte{ 0x88, 26, 0x89, 24, }, key[:]...) m1 := Metadata{ ManagementKey: &DefaultManagementKey, } raw, err := m1.marshal() if err != nil { t.Fatalf("marshaling key: %v", err) } m2 := Metadata{ ManagementKey: &key, raw: raw, } got, err := m2.marshal() if err != nil { t.Fatalf("marshaling updated metadata: %v", err) } if !bytes.Equal(want, got) { t.Errorf("(*Metadata.marshal, got=0x%x, want=0x%x", got, want) } } func TestMetadataAdditoinalFields(t *testing.T) { key := [24]byte{ 0x09, 0xd9, 0x87, 0x81, 0xfb, 0xdc, 0xc9, 0xb6, 0x91, 0xa2, 0x05, 0x80, 0x6e, 0xc0, 0xba, 0x84, 0x31, 0xac, 0x0d, 0x9f, 0x59, 0xa5, 0x00, 0xad, } raw := []byte{ 0x88, 4, // Unrecognized sub-object. The key should be added, but this object // shouldn't be impacted. 0x87, 2, 0x00, 0x01, } want := append([]byte{ 0x88, 30, // Unrecognized sub-object. 0x87, 2, 0x00, 0x01, // Added management key. 0x89, 24, }, key[:]...) m := Metadata{ ManagementKey: &key, raw: raw, } got, err := m.marshal() if err != nil { t.Fatalf("marshaling updated metadata: %v", err) } if !bytes.Equal(want, got) { t.Errorf("(*Metadata.marshal, got=0x%x, want=0x%x", got, want) } }