Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for YubiKey firmware 5.7 specifics #47

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2023 Steffen Vogel <post@steffenvogel.de>
# SPDX-License-Identifier: Apache-2.0

mockdata/** linguist-generated
116 changes: 90 additions & 26 deletions algorithm.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
// SPDX-FileCopyrightText: 2020 Google LLC
// SPDX-FileCopyrightText: 2023-2024 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0

package piv

type algorithmType byte

const (
AlgTypeRSA algorithmType = iota + 1
AlgTypeECCP
AlgTypeEd25519
)
import "fmt"

// Algorithm represents a specific algorithm and bit size supported by the PIV
// specification.
Expand All @@ -18,48 +13,117 @@ type Algorithm byte
// 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
// For algorithm discovery, see: https://github.com/go-piv/piv-go/issues/1
const (
Alg3DES Algorithm = 0x03
AlgRSA1024 Algorithm = 0x06
AlgRSA2048 Algorithm = 0x07
AlgECCP256 Algorithm = 0x11
AlgECCP384 Algorithm = 0x14

// Non-standard; as implemented by SoloKeys. Chosen for low probability of eventual
// clashes, if and when PIV standard adds Ed25519 support
AlgEd25519 Algorithm = 0x22
// NIST SP 800-78-4
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=21
AlgRSA2048 Algorithm = 0x07 // RSA 2048 bit modulus, 65537 ≤ exponent ≤ 2256 - 1
AlgECCP256 Algorithm = 0x11 // ECC: Curve P-256
AlgECCP384 Algorithm = 0x14 // ECC: Curve P-384

// NIST SP 800-78-5 ipd (Initial Public Draft)
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-78-5.ipd.pdf#page=12
Alg3DESSalt Algorithm = 0x00 // 3 Key Triple DES – ECB (deprecated)
Alg3DES Algorithm = 0x03 // 3 Key Triple DES – ECB (deprecated)
AlgRSA3072 Algorithm = 0x05 // RSA 3072 bit modulus, 65537 ≤ exponent ≤ 2256 - 1
AlgRSA1024 Algorithm = 0x06 // RSA 1024 bit modulus, 65537 ≤ exponent ≤ 2256 - 1
AlgAES128 Algorithm = 0x08 // AES-128 – ECB
AlgAES192 Algorithm = 0x0A // AES-192 – ECB
AlgAES256 Algorithm = 0x0C // AES-256 – ECB
AlgCS2 Algorithm = 0x27 // Cipher Suite 2
AlgCS7 Algorithm = 0x2E // Cipher Suite 7

// Non-standard extensions
AlgPIN Algorithm = 0xFF

// YubiKey 5.7 Firmware Specifics - PIV Enhancements - Additional Key Types Supported
//
// https://docs.yubico.com/hardware/yubikey/yk-tech-manual/5.7-firmware-specifics.html#additional-key-types-supported
AlgRSA4096 Algorithm = 0x16

AlgEd25519 Algorithm = 0xE0 // YubiKey
AlgX25519 Algorithm = 0xE1 // YubiKey

// Trussed PIV authenticator (NitroKey / SoloKeys)
//
// https://github.com/Nitrokey/piv-authenticator/blob/efb4632b3f498af6732fc716354af746f3960038/tests/command_response.rs#L58-L72

// AlgECCP521 Algorithm = 0x15
// AlgRSA3072 Algorithm = 0xE0
// AlgRSA4096 Algorithm = 0xE1
// AlgEd25519 Algorithm = 0xE2
// AlgX25519 Algorithm = 0xE3
// AlgEd448 Algorithm = 0xE4
// AlgX448 Algorithm = 0xE5

// Internal algorithms for testing
algRSA512 Algorithm = 0xF0
algECCP224 Algorithm = 0xF1
algECCP521 Algorithm = 0xF2
)

func (a Algorithm) algType() algorithmType {
func (a Algorithm) String() string {
switch a {
case AlgRSA1024, AlgRSA2048:
return AlgTypeRSA
case AlgRSA1024, AlgRSA2048, AlgRSA3072, AlgRSA4096, algRSA512:
return fmt.Sprintf("RSA-%d", a.bits())

case AlgECCP256, AlgECCP384, algECCP224, algECCP521:
return fmt.Sprintf("P-%d", a.bits())

case Alg3DESSalt:
return "3DESSalt"
case Alg3DES:
return "3DES"

case AlgAES128, AlgAES192, AlgAES256:
return fmt.Sprintf("AES-%d", a.bits())

case AlgECCP256, AlgECCP384:
return AlgTypeECCP
case AlgCS2:
return "CS2"
case AlgCS7:
return "CS7"

case AlgPIN:
return "PIN"

case AlgEd25519:
return AlgTypeEd25519
return "Ed25519"
case AlgX25519:
return "X25519"

default:
return 0
return ""
}
}

func (a Algorithm) bits() int {
switch a {
case algRSA512:
return 512
case AlgRSA1024:
return 1024
case AlgRSA2048:
return 2048
case AlgRSA3072:
return 3072
case AlgRSA4096:
return 4096

case algECCP224:
return 224
case AlgECCP256:
return 256
case AlgECCP384:
return 384
case algECCP521:
return 521

case AlgAES128:
return 128
case AlgAES192:
return 192
case AlgAES256:
return 256

default:
return 0
Expand Down
7 changes: 5 additions & 2 deletions attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import (
)

// 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.
// on a card.
//
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
type Attestation struct {
// Version of the YubiKey's firmware.
Version iso.Version
Expand Down Expand Up @@ -176,6 +178,7 @@ func (c *Card) AttestationCertificate() (*x509.Certificate, error) {
// 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
Expand Down
2 changes: 1 addition & 1 deletion attestation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestAttestation(t *testing.T) {
serial, err := c.Serial()
assert.NoError(t, err, "Failed to get serial number")
assert.Equal(t, serial, a.Serial, "Mismatching attestation serial got=%d, wanted=%d", a.Serial, serial)
assert.Equal(t, key.PINPolicy, a.PINPolicy, "Mismatching attestation pin policy got=0x%x, wanted=0x%x", a.TouchPolicy, key.PINPolicy)
assert.Equal(t, key.PINPolicy, a.PINPolicy, "Mismatching attestation PIN policy got=0x%x, wanted=0x%x", a.TouchPolicy, key.PINPolicy)
assert.Equal(t, key.TouchPolicy, a.TouchPolicy, "Mismatching attestation touch policy got=0x%x, wanted=0x%x", a.TouchPolicy, key.TouchPolicy)
assert.Equal(t, c.Version(), a.Version, "Mismatching attestation version got=%#v, wanted=%#v", a.Version, c.Version())
assert.Equal(t, SlotAuthentication, a.Slot, "Mismatching attested slot got=%v, wanted=%v", a.Slot, SlotAuthentication)
Expand Down
92 changes: 69 additions & 23 deletions auth.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// SPDX-FileCopyrightText: 2023 Steffen Vogel <post@steffenvogel.de>
// SPDX-FileCopyrightText: 2023-2024 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0

package piv

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/des" //nolint:gosec
"errors"
"fmt"
Expand All @@ -21,12 +23,17 @@ var errFailedToGenerateKey = errors.New("failed to generate random key")
// certificates to slots.
//
// Use DefaultManagementKey if the management key hasn't been set.
//
// 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
func (c *Card) authenticate(key ManagementKey) 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
meta, err := c.Metadata(SlotCardManagement)
if err != nil {
return fmt.Errorf("failed to get management key metadata: %w", err)
}

// Request a witness
resp, err := sendTLV(c.tx, iso.InsGeneralAuthenticate, byte(Alg3DES), keyCardManagement,
resp, err := sendTLV(c.tx, iso.InsGeneralAuthenticate, byte(meta.Algorithm), keyCardManagement,
tlv.New(0x7c,
tlv.New(0x80),
),
Expand All @@ -35,30 +42,41 @@ func (c *Card) authenticate(key ManagementKey) error {
return fmt.Errorf("failed to execute command: %w", err)
}

var block cipher.Block

switch meta.Algorithm {
case Alg3DES:
block, err = des.NewTripleDESCipher(key[:]) //nolint:gosec

case AlgAES128, AlgAES192, AlgAES256:
block, err = aes.NewCipher(key[:])

default:
return errUnsupportedKeyType
}
if err != nil {
return fmt.Errorf("failed to create block cipher: %w", err)
}

cardChallenge, _, ok := resp.GetChild(0x7c, 0x80)
if !ok {
return errUnmarshal
} else if len(cardChallenge) != 8 {
return errUnexpectedLength
}

block, err := des.NewTripleDESCipher(key[:]) //nolint:gosec
if err != nil {
return fmt.Errorf("failed to create triple des block cipher: %w", err)
} else if len(cardChallenge) != block.BlockSize() {
return fmt.Errorf("%w: %d", errUnexpectedLength, len(cardChallenge))
}

cardResponse := make([]byte, 8)
cardResponse := make([]byte, block.BlockSize())
block.Decrypt(cardResponse, cardChallenge)

challenge := make([]byte, 8)
challenge := make([]byte, block.BlockSize())
if _, err := io.ReadFull(c.Rand, challenge); err != nil {
return fmt.Errorf("failed to read random data: %w", err)
}

response := make([]byte, 8)
response := make([]byte, block.BlockSize())
block.Encrypt(response, challenge)

if resp, err = sendTLV(c.tx, iso.InsGeneralAuthenticate, byte(Alg3DES), keyCardManagement,
if resp, err = sendTLV(c.tx, iso.InsGeneralAuthenticate, byte(meta.Algorithm), keyCardManagement,
tlv.New(0x7c,
tlv.New(0x80, cardResponse),
tlv.New(0x81, challenge),
Expand All @@ -69,7 +87,7 @@ func (c *Card) authenticate(key ManagementKey) error {

if cardResponse, _, ok = resp.GetChild(0x7c, 0x82); !ok {
return errUnmarshal
} else if len(cardResponse) != 8 {
} else if len(cardResponse) != block.BlockSize() {
return errUnexpectedLength
} else if !bytes.Equal(cardResponse, response) {
return errChallengeFailed
Expand All @@ -79,6 +97,7 @@ func (c *Card) authenticate(key ManagementKey) error {
}

// authenticateWithPIN uses a PIN protected management key to authenticate
//
// https://docs.yubico.com/yesdk/users-manual/application-piv/pin-only.html
// https://docs.yubico.com/yesdk/users-manual/application-piv/piv-objects.html#pinprotecteddata
//
Expand Down Expand Up @@ -108,19 +127,18 @@ func (c *Card) authenticateWithPIN(pin string) error {
// if err := c.SetManagementKey(piv.DefaultManagementKey, newKey); err != nil {
// // ...
// }
func (c *Card) SetManagementKey(oldKey, newKey ManagementKey) error {
func (c *Card) SetManagementKey(oldKey, newKey ManagementKey, requireTouch bool, alg Algorithm) error {
if err := c.authenticate(oldKey); err != nil {
return fmt.Errorf("failed to authenticate with old key: %w", err)
}

p2 := byte(0xff)
touch := false // TODO
if touch {
if requireTouch {
p2 = 0xfe
}

if _, err := send(c.tx, insSetManagementKey, 0xff, p2, append([]byte{
byte(Alg3DES), keyCardManagement, 24,
byte(alg), keyCardManagement, 24,
}, newKey[:]...)); err != nil {
return fmt.Errorf("failed to execute command: %w", err)
}
Expand All @@ -130,7 +148,7 @@ func (c *Card) SetManagementKey(oldKey, newKey ManagementKey) error {

// https://docs.yubico.com/yesdk/users-manual/application-piv/pin-only.html
// https://docs.yubico.com/yesdk/users-manual/application-piv/piv-objects.html#pinprotecteddata
func (c *Card) SetManagementKeyPinProtected(oldKey ManagementKey, pin string) error {
func (c *Card) SetManagementKeyPinProtected(oldKey ManagementKey, pin string, requireTouch bool, alg Algorithm) error {
var newKey ManagementKey

if n, err := c.Rand.Read(newKey[:]); err != nil {
Expand All @@ -152,7 +170,7 @@ func (c *Card) SetManagementKeyPinProtected(oldKey ManagementKey, pin string) er
return err
}

return c.SetManagementKey(oldKey, newKey)
return c.SetManagementKey(oldKey, newKey, requireTouch, alg)
}

// SetPIN updates the PIN to a new value. For compatibility, PINs should be 1-8
Expand Down Expand Up @@ -251,6 +269,10 @@ func encodePIN(pin string) ([]byte, error) {
}

// Apply padding
//
// 2.4 Security Architecture
// 2.4.3 Authentication of an Individual
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=88
for i := len(data); i < 8; i++ {
data = append(data, 0xff)
}
Expand Down Expand Up @@ -278,7 +300,11 @@ func login(tx *iso.Transaction, pin string) error {
return err
}

// 3.2 PIV Card Application Card Commands for Authentication
// 3.2.1 VERIFY Card Command
//
// https://csrc.nist.gov/CSRC/media/Publications/sp/800-73/4/archive/2015-05-29/documents/sp800_73-4_pt2_draft.pdf#page=20
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=86
if _, err = send(tx, iso.InsVerify, 0, 0x80, data); err != nil {
return fmt.Errorf("failed to execute command: %w", err)
}
Expand All @@ -295,7 +321,7 @@ func loginNeeded(tx *iso.Transaction) bool {
func (c *Card) Retries() (int, error) {
_, err := send(c.tx, iso.InsVerify, 0, 0x80, nil)
if err == nil {
return 0, fmt.Errorf("%w from empty pin", errExpectedError)
return 0, fmt.Errorf("%w from empty PIN", errExpectedError)
}

var aErr AuthError
Expand All @@ -305,3 +331,23 @@ func (c *Card) Retries() (int, error) {

return 0, fmt.Errorf("invalid response: %w", err)
}

// SetRetries sets the number of attempts for PIN and PUK.
//
// Both PIN and PUK will be reset to default values when this is executed.
// Requires authentication with management key and PIN verification.
func (c *Card) SetRetries(key ManagementKey, pin string, pinAttempts, pukAttempts int) error {
if err := login(c.tx, pin); err != nil {
return fmt.Errorf("PIN verification failed: %w", err)
}

if err := c.authenticate(key); err != nil {
return fmt.Errorf("failed to authenticate with management key: %w", err)
}

if _, err := send(c.tx, insSetPINRetries, byte(pinAttempts), byte(pukAttempts), nil); err != nil {
return fmt.Errorf("failed to execute command: %w", err)
}

return nil
}
Loading
Loading