Skip to content

Commit

Permalink
Merge branch 'jens/bio-support'
Browse files Browse the repository at this point in the history
  • Loading branch information
jensutbult committed Jun 18, 2024
2 parents b474825 + 8343e4f commit 8b4ddca
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 22 deletions.
22 changes: 22 additions & 0 deletions FullStackTests/Tests/ManagementFullStackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,28 @@ class ManagementFullStackTests: XCTestCase {
print("⚠️ Note that no more NFC testing will be possible until NFC restriction has been disabled for this key!")
}
}

func testBioDeviceReset() throws {
runManagementTest { connection, session, transport in
let deviceInfo = try await session.getDeviceInfo()
guard deviceInfo.formFactor == .usbCBio || deviceInfo.formFactor == .usbABio else {
print("⚠️ Skip testBioDeviceReset()")
return
}
try await session.deviceReset()
var pivSession = try await PIVSession.session(withConnection: connection)
var pinMetadata = try await pivSession.getPinMetadata()
XCTAssertTrue(pinMetadata.isDefault)
try await pivSession.setPin("654321", oldPin: "123456")
pinMetadata = try await pivSession.getPinMetadata()
XCTAssertFalse(pinMetadata.isDefault)
let managementSession = try await ManagementSession.session(withConnection: connection)
try await managementSession.deviceReset()
pivSession = try await PIVSession.session(withConnection: connection)
pinMetadata = try await pivSession.getPinMetadata()
XCTAssertTrue(pinMetadata.isDefault)
}
}
}

extension XCTestCase {
Expand Down
87 changes: 78 additions & 9 deletions FullStackTests/Tests/PIVFullStackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -622,9 +622,86 @@ final class PIVFullStackTests: XCTestCase {
}

}

// This will test auth on a YubiKey Bio. To run the test at least one fingerprint needs to be registered.
func testBioAuthentication() throws {
runAsyncTest {
let connection = try await AllowedConnections.anyConnection()
let managementSession = try await ManagementSession.session(withConnection: connection)
let deviceInfo = try await managementSession.getDeviceInfo()
guard deviceInfo.formFactor == .usbCBio || deviceInfo.formFactor == .usbABio else {
print("⚠️ Skip testBioAuthentication()")
return
}
let pivSession = try await PIVSession.session(withConnection: connection)
var bioMetadata = try await pivSession.getBioMetadata()
if !bioMetadata.isConfigured {
let message = "No fingerprints registered for this yubikey or there's an error in getBioMetadata()."
print("⚠️ \(message)")
XCTFail(message)
return
}
XCTAssertTrue(bioMetadata.attemptsRemaining > 0)
var verifyResult = try await pivSession.verifyUv(requestTemporaryPin: false, checkOnly: false)
XCTAssertNil(verifyResult)
Logger.test.debug("✅ verifyUV() passed")
guard let pinData = try await pivSession.verifyUv(requestTemporaryPin: true, checkOnly: false) else {
XCTFail("Pin data returned was nil. Expected a value.")
return
}
Logger.test.debug("✅ got temporary pin: \(pinData.hexEncodedString).")
bioMetadata = try await pivSession.getBioMetadata()
XCTAssertTrue(bioMetadata.temporaryPin)
Logger.test.debug("✅ temporary pin reported as set.")
verifyResult = try await pivSession.verifyUv(requestTemporaryPin: false, checkOnly: true)
XCTAssertNil(verifyResult)
Logger.test.debug("✅ verifyUv successful.")
try await pivSession.verifyTemporaryPin(pinData)
Logger.test.debug("✅ temporary pin verified.")
}
}

func testBioPinPolicyErrorOnNonBioKey() throws {
runAsyncTest {
let connection = try await AllowedConnections.anyConnection()
let managementSession = try await ManagementSession.session(withConnection: connection)
let deviceInfo = try await managementSession.getDeviceInfo()
guard deviceInfo.formFactor != .usbCBio && deviceInfo.formFactor != .usbABio else {
print("⚠️ Skip testBioPinPolicyErrorOnNonBioKey() since this is a bio key.")
return
}
let session = try await PIVSession.session(withConnection: connection)
try await self.authenticate(with: session)
do {
_ = try await session.generateKeyInSlot(slot: .signature, type: .ECCP384, pinPolicy: .matchAlways, touchPolicy: .defaultPolicy)
} catch {
guard let sessionError = error as? SessionError else { throw error }
XCTAssertEqual(sessionError, SessionError.notSupported)
}
do {
_ = try await session.generateKeyInSlot(slot: .signature, type: .ECCP384, pinPolicy: .matchOnce, touchPolicy: .defaultPolicy)
} catch {
guard let sessionError = error as? SessionError else { throw error }
XCTAssertEqual(sessionError, SessionError.notSupported)
}
}
}
}

extension XCTestCase {

func authenticate(with session: PIVSession) async throws {
let defaultManagementKey = Data(hexEncodedString: "010203040506070801020304050607080102030405060708")!
let keyType: PIVManagementKeyType
if session.supports(PIVSessionFeature.metadata) {
let metadata = try await session.getManagementKeyMetadata()
keyType = metadata.keyType
} else {
keyType = .tripleDES
}
try await session.authenticateWith(managementKey: defaultManagementKey, keyType: keyType)
}

func runPIVTest(named testName: String = #function,
in file: StaticString = #file,
at line: UInt = #line,
Expand All @@ -649,15 +726,7 @@ extension XCTestCase {
let connection = try await AllowedConnections.anyConnection()
let session = try await PIVSession.session(withConnection: connection)
try await session.reset()
let defaultManagementKey = Data(hexEncodedString: "010203040506070801020304050607080102030405060708")!
let keyType: PIVManagementKeyType
if session.supports(PIVSessionFeature.metadata) {
let metadata = try await session.getManagementKeyMetadata()
keyType = metadata.keyType
} else {
keyType = .tripleDES
}
try await session.authenticateWith(managementKey: defaultManagementKey, keyType: keyType)
try await self.authenticate(with: session)
Logger.test.debug("⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ PIV Session test ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️")
try await test(session)
Logger.test.debug("\(testName) passed")
Expand Down
3 changes: 2 additions & 1 deletion FullStackTests/allowed-yubikeys.csv
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
14453003,
27365403,
9681623,
27365450
27365450,
28293730
2 changes: 1 addition & 1 deletion YubiKit/YubiKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@
B456E21A274FCA26004471DE /* ManagementSession.swift */,
B40528322987C31E00FC33AB /* DeviceInfo.swift */,
B405283629894E7600FC33AB /* DeviceConfig.swift */,
B49F90C32B9F30A400C10F0B /* ManagementFeature.swift */,
B400642F2BF728D600CD2FAF /* Capability.swift */,
B49F90C32B9F30A400C10F0B /* ManagementFeature.swift */,
);
path = Management;
sourceTree = "<group>";
Expand Down
10 changes: 9 additions & 1 deletion YubiKit/YubiKit/Management/ManagementFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,24 @@

import Foundation

/// Management session features.
public enum ManagementFeature: SessionFeature {

case deviceInfo, deviceConfig
/// Support for reading the DeviceInfo data from the YubiKey.
case deviceInfo
/// Support for writing DeviceConfig data to the YubiKey.
case deviceConfig
/// Support for device-wide reset
case deviceReset

public func isSupported(by version: Version) -> Bool {
switch self {
case .deviceInfo:
return version >= Version(withString: "4.1.0")!
case .deviceConfig:
return version >= Version(withString: "5.0.0")!
case .deviceReset:
return version >= Version(withString: "5.6.0")!
}
}
}
14 changes: 12 additions & 2 deletions YubiKit/YubiKit/Management/ManagementSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public final actor ManagementSession: Session, InternalSession {

/// Returns the DeviceInfo for the connected YubiKey.
///
/// >Note: This functionality requires support for device info available on YubiKey 4.1 or later.
/// >Note: This functionality requires support for ``ManagementFeature/deviceInfo``, available on YubiKey 4.1 or later.
public func getDeviceInfo() async throws -> DeviceInfo {
Logger.management.debug("\(String(describing: self).lastComponent), \(#function)")
guard self.supports(ManagementFeature.deviceInfo) else { throw SessionError.notSupported }
Expand All @@ -104,7 +104,7 @@ public final actor ManagementSession: Session, InternalSession {

/// Write device config to a YubiKey 5 or later.
///
/// >Note: This functionality requires support for device config, available on YubiKey 5 or later.
/// >Note: This functionality requires support for ``ManagementFeature/deviceConfig``, available on YubiKey 5 or later.
///
/// - Parameters:
/// - config: The device configuration to write.
Expand Down Expand Up @@ -164,6 +164,16 @@ public final actor ManagementSession: Session, InternalSession {
try await setEnabled(true, application: application, overTransport: transport, reboot: reboot)
}

/// Perform a device-wide reset in Bio Multi-protocol Edition devices.
///
/// >Note: This functionality requires support for ``ManagementFeature/deviceReset``, available on YubiKey 5.6 or later.
public func deviceReset() async throws {
guard self.supports(ManagementFeature.deviceReset) else { throw SessionError.notSupported }
guard let connection = _connection else { throw SessionError.noConnection }
let apdu = APDU(cla: 0, ins: 0x1f, p1: 0, p2: 0)
try await connection.send(apdu: apdu)
}

deinit {
Logger.management.debug("\(String(describing: self).lastComponent), \(#function)")
}
Expand Down
31 changes: 31 additions & 0 deletions YubiKit/YubiKit/PIV/PIVDataTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public enum PIVPinPolicy: UInt8 {
case once = 0x2
/// The PIN must be verified each time the key is to be used, just prior to using it.
case always = 0x3
/// PIN or biometrics must be verified for the session, prior to using the key.
case matchOnce = 0x4
/// PIN or biometrics must be verified each time the key is to be used, just prior to using it.
case matchAlways = 0x5
};

/// The slot to use in the PIV application.
Expand Down Expand Up @@ -235,3 +239,30 @@ public enum PIVManagementKeyType: UInt8 {
}
}
}

/// Metadata about a Bio multi-protocol YubiKey.
public struct PIVBioMetadata {
/// Indicates whether biometrics are configured or not (fingerprints enrolled or not).
///
/// A false return value indicates a YubiKey Bio without biometrics configured and hence the
/// client should fallback to a PIN based authentication.
public let isConfigured: Bool

/// Value of biometric match retry counter which states how many biometric match retries
/// are left until a YubiKey Bio is blocked.
///
/// If this method returns 0 and ``isConfigured`` returns true, the device is blocked for
/// biometric match and the client should invoke PIN based authentication to reset the biometric
/// match retry counter.
public let attemptsRemaining: UInt

/// Indicates whether a temporary PIN has been generated in the YubiKey in relation to a
/// successful biometric match. Is true if a temporary PIN has been generated.
public let temporaryPin: Bool

internal init(isConfigured: Bool, attemptsRemaining: UInt, temporaryPin: Bool) {
self.isConfigured = isConfigured
self.attemptsRemaining = attemptsRemaining
self.temporaryPin = temporaryPin
}
}
Loading

0 comments on commit 8b4ddca

Please sign in to comment.