Skip to content

Commit

Permalink
Add support for more card features:
Browse files Browse the repository at this point in the history
- User Interaction Flag
- Key Import
- EC Signatures
- ECDH
- Better tests

Signed-off-by: Steffen Vogel <[email protected]>
  • Loading branch information
stv0g committed Dec 3, 2023
1 parent 02c895b commit dad6391
Show file tree
Hide file tree
Showing 78 changed files with 2,426 additions and 574 deletions.
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,28 @@ SPDX-License-Identifier: Apache-2.0
- [x] Public Key URL
- [x] Login data
- [x] Private data
- [ ] User interaction flag
- [x] User interaction flag
- [x] Password status
- [ ] Key Import
- [x] AES
- [ ] RSA
- [ ] EC
- [x] RSA
- [x] ECDSA
- [x] EdDSA
- [x] 7.2.9 `GET RESPONSE`
- [ ] 7.2.10 `PSO: COMPUTE DIGITAL SIGNATURE`
- [ ] RSA
- [ ] ECDSA
- [ ] Attestation
- [x] ECDSA
- [x] EdDSA
- [ ] 7.2.11 `PSO: DECIPHER`
- [x] AES
- [ ] RSA
- [ ] ECDH
- [x] ECDH
- [x] EdDSA
- [ ] 7.2.12 `PSO: ENCIPHER`
- [x] AES
- [ ] 7.2.13 `INTERNAL AUTHENTICATE`
- [ ] RSA
- [ ] ECDSA
- [x] 7.2.14 `GENERATE ASYMMETRIC KEY PAIR`
- [x] RSA
- [x] Elliptic Curves
Expand All @@ -69,6 +74,8 @@ SPDX-License-Identifier: Apache-2.0
- [x] 7.2.18 `MANAGE SECURITY ENVIRONMENT`

- [x] Key Derivation Function (KDF) for `VERIFY`
- [ ] Attestation
- [ ] PIN Handler / Callback

### YubiKey extensions

Expand Down
146 changes: 146 additions & 0 deletions algs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// SPDX-FileCopyrightText: 2023 Steffen Vogel <[email protected]>
// SPDX-License-Identifier: Apache-2.0

package openpgp

import (
"bytes"
"encoding/binary"
"fmt"
"reflect"
)

type AlgorithmAttributes struct {
Algorithm AlgPubkey
ImportFormat ImportFormat

// Relevant for RSA
LengthModulus int
LengthExponent int

// Relevant for ECDSA/ECDH/EdDSA
OID []byte
}

func (a AlgorithmAttributes) Compatible(b AlgorithmAttributes) bool {
if b.OID != nil { // EC
return bytes.Equal(a.OID, b.OID)
}

return a.LengthModulus == b.LengthModulus // RSA
}

func (a AlgorithmAttributes) Equal(b AlgorithmAttributes) bool {
return reflect.DeepEqual(a, b)
}

func (a *AlgorithmAttributes) Decode(b []byte) error {
if len(b) < 1 {
return ErrInvalidLength
}

a.Algorithm = AlgPubkey(b[0])

switch a.Algorithm {
case AlgPubkeyRSA:
if len(b) < 6 {
return ErrInvalidLength
}

a.LengthModulus = int(binary.BigEndian.Uint16(b[1:]))
a.LengthExponent = int(binary.BigEndian.Uint16(b[3:]))
a.ImportFormat = ImportFormat(b[5])

case AlgPubkeyECDH, AlgPubkeyECDSA, AlgPubkeyEdDSA:
a.OID = b[1:]

// Strip trailing import format byte if present
l := len(a.OID)
if ImportFormat(a.OID[l-1]) == ImportFormatECDSAStdWithPublicKey {
a.ImportFormat = ImportFormatECDSAStdWithPublicKey
a.OID = a.OID[:l-1]
}

default:
return errUnmarshal
}

return nil
}

func (a AlgorithmAttributes) Encode() (b []byte) {
b = []byte{byte(a.Algorithm)}

switch a.Algorithm {
case AlgPubkeyRSA:
b = binary.BigEndian.AppendUint16(b, uint16(a.LengthModulus))
b = binary.BigEndian.AppendUint16(b, uint16(a.LengthExponent))
b = append(b, byte(a.ImportFormat))

case AlgPubkeyECDH, AlgPubkeyECDSA, AlgPubkeyEdDSA:
b = append(b, a.OID...)
if a.ImportFormat == ImportFormatECDSAStdWithPublicKey {
b = append(b, byte(ImportFormatECDSAStdWithPublicKey))
}

default:
}

return b
}

func (a AlgorithmAttributes) String() string {
switch a.Algorithm {
case AlgPubkeyRSAEncOnly, AlgPubkeyRSASignOnly, AlgPubkeyRSA:
return fmt.Sprintf("RSA-%d", a.LengthModulus)

case AlgPubkeyECDH, AlgPubkeyECDSA, AlgPubkeyEdDSA:
return fmt.Sprintf("%s (%s)", a.Curve(), a.Algorithm)

default:
return "<unknown>"
}
}

func (a AlgorithmAttributes) Curve() Curve {
switch {
case bytes.Equal(a.OID, oidANSIx9p256r1):
return CurveANSIx9p256r1
case bytes.Equal(a.OID, oidANSIx9p384r1):
return CurveANSIx9p384r1
case bytes.Equal(a.OID, oidANSIx9p521r1):
return CurveANSIx9p521r1

case bytes.Equal(a.OID, oidBrainpoolP256r1):
return CurveBrainpoolP256r1
case bytes.Equal(a.OID, oidBrainpoolP384r1):
return CurveBrainpoolP384r1
case bytes.Equal(a.OID, oidBrainpoolP512r1):
return CurveBrainpoolP512r1

case bytes.Equal(a.OID, oidSecp256k1):
return CurveSecp256k1

case bytes.Equal(a.OID, oidEd448):
return CurveEd448
case bytes.Equal(a.OID, oidEd25519):
return CurveEd25519

case bytes.Equal(a.OID, oidX448):
return CurveX448
case bytes.Equal(a.OID, oidX25519):
return CurveX25519
}

return CurveUnknown
}

func EC(curve Curve) AlgorithmAttributes {
return curve.AlgAttrs()
}

func RSA(bits int) AlgorithmAttributes {
return AlgorithmAttributes{
LengthModulus: bits,
}
}
38 changes: 37 additions & 1 deletion auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"

iso "cunicu.li/go-iso7816"
"cunicu.li/go-iso7816/encoding/tlv"
)

// VerifyPassword attempts to unlock a given password.
Expand Down Expand Up @@ -72,7 +73,7 @@ func (c *Card) ChangePassword(pwType byte, pwCurrent, pwNew string) error {
}

default:
return errUnsupported
return ErrUnsupported
}

_, err := send(c.tx, iso.InsChangeReferenceData, 0x00, pwType, []byte(pwCurrent+pwNew))
Expand Down Expand Up @@ -130,3 +131,38 @@ func (c *Card) SetRetryCounters(pw1, rc, pw3 byte) error {
_, err := send(c.tx, insSetPINRetries, 0, 0, []byte{pw1, rc, pw3})
return err
}

func (c *Card) SetUserInteractionMode(op SecurityOperation, mode UserInteractionMode, feat GeneralFeatures) error {
uif := UIF{mode, feat}
return c.putData(tagUIFSign+tlv.Tag(op), uif.Encode())
}

type PasswordMode struct {
RequirePW1ForEachSignature bool
UsePINBlockFormat2ForPW1 bool
}

func (c *Card) SetPasswordMode(mode PasswordMode) error {
sts, err := c.getData(tagPasswordStatus)
if err != nil {
return err
}

if mode.RequirePW1ForEachSignature {
sts[0] = 0
} else {
sts[0] = 1
}

if mode.UsePINBlockFormat2ForPW1 {
if c.Capabilities.Pin2BlockFormat == 0 {
return fmt.Errorf("PIN block 2 format is %w", ErrUnsupported)
}

sts[1] |= 0b1
} else {
sts[1] &= 0b1
}

return c.putData(tagPasswordStatus, sts)
}
10 changes: 5 additions & 5 deletions auth_kdf.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import (

var (
errIterationsTooSmall = errors.New("iterations too small")
errUnsupportedKDFAlg = fmt.Errorf("%w algorithm", errUnsupported)
errUnsupportedKDFHashAlg = fmt.Errorf("%w hash algorithm", errUnsupported)
errUnsupportedPasswordLengthsTooShortForKDF = fmt.Errorf("%w: password lengths are too small for KDF", errUnsupported)
errUnsupportedKDFAlg = fmt.Errorf("%w algorithm", ErrUnsupported)
errUnsupportedKDFHashAlg = fmt.Errorf("%w hash algorithm", ErrUnsupported)
errUnsupportedPasswordLengthsTooShortForKDF = fmt.Errorf("%w: password lengths are too small for KDF", ErrUnsupported)
errMissingKDFSalt = errors.New("missing salt")
)

Expand Down Expand Up @@ -48,7 +48,7 @@ func (c *Card) GetKDF() (k *KDF, err error) {
func (c *Card) SetupKDF(alg AlgKDF, iterations int, pw1, pw3 string) (err error) {
// Check if KDF is supported
if c.Capabilities.Flags&CapKDF == 0 {
return fmt.Errorf("key derived passwords are %w", errUnsupported)
return fmt.Errorf("key derived passwords are %w", ErrUnsupported)
}

if min(c.PasswordStatus.LengthPW1, c.PasswordStatus.LengthRC, c.PasswordStatus.LengthPW3) < 64 {
Expand Down Expand Up @@ -84,7 +84,7 @@ func (c *Card) SetupKDF(alg AlgKDF, iterations int, pw1, pw3 string) (err error)
}

default:
return errUnsupported
return ErrUnsupported
}

if kdf.InitialHashPW1, err = kdf.DerivePassword(PW1, DefaultPW1); err != nil {
Expand Down
Loading

0 comments on commit dad6391

Please sign in to comment.