pax_global_header 0000666 0000000 0000000 00000000064 14071735634 0014524 g ustar 00root root 0000000 0000000 52 comment=23adea5d745fded4128735d10e1843346aa7a707
piv-go-1.8.0/ 0000775 0000000 0000000 00000000000 14071735634 0012733 5 ustar 00root root 0000000 0000000 piv-go-1.8.0/.github/ 0000775 0000000 0000000 00000000000 14071735634 0014273 5 ustar 00root root 0000000 0000000 piv-go-1.8.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14071735634 0016330 5 ustar 00root root 0000000 0000000 piv-go-1.8.0/.github/workflows/test.yaml 0000664 0000000 0000000 00000001706 14071735634 0020177 0 ustar 00root root 0000000 0000000 name: 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.md 0000664 0000000 0000000 00000002111 14071735634 0015157 0 ustar 00root root 0000000 0000000 # 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/LICENSE 0000664 0000000 0000000 00000026136 14071735634 0013750 0 ustar 00root root 0000000 0000000
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/Makefile 0000664 0000000 0000000 00000000210 14071735634 0014364 0 ustar 00root root 0000000 0000000 .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.md 0000664 0000000 0000000 00000017650 14071735634 0014223 0 ustar 00root root 0000000 0000000 This is not an officially supported Google product
# A Go YubiKey PIV implementation
[](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.mod 0000664 0000000 0000000 00000000051 14071735634 0014035 0 ustar 00root root 0000000 0000000 module github.com/go-piv/piv-go
go 1.13
piv-go-1.8.0/piv/ 0000775 0000000 0000000 00000000000 14071735634 0013531 5 ustar 00root root 0000000 0000000 piv-go-1.8.0/piv/doc.go 0000664 0000000 0000000 00000001250 14071735634 0014623 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000114441 14071735634 0014655 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000053205 14071735634 0015714 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000011415 14071735634 0015012 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000002136 14071735634 0016356 0 ustar 00root root 0000000 0000000 // 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_errors 0000664 0000000 0000000 00000013074 14071735634 0016005 0 ustar 00root root 0000000 0000000 SCARD_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.go 0000664 0000000 0000000 00000012023 14071735634 0016402 0 ustar 00root root 0000000 0000000 // 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.py 0000775 0000000 0000000 00000003713 14071735634 0016436 0 ustar 00root root 0000000 0000000 #!/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.go 0000664 0000000 0000000 00000001530 14071735634 0016501 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000001541 14071735634 0016230 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000007274 14071735634 0016061 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000007135 14071735634 0016061 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000011115 14071735634 0016561 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000054553 14071735634 0014672 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000026473 14071735634 0015731 0 ustar 00root root 0000000 0000000 // 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)
}
}