From d076677d5f63c23bb1a0a5ee90fb74e59b36ff99 Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Fri, 17 May 2024 13:37:14 +0200 Subject: [PATCH 1/7] Implemented paging when reading device info and new properties for the DeviceInfo. --- .../Tests/ManagementFullStackTests.swift | 50 ++++++---- YubiKit/YubiKit/Management/Capability.swift | 51 ++++++++++ YubiKit/YubiKit/Management/DeviceConfig.swift | 5 +- YubiKit/YubiKit/Management/DeviceInfo.swift | 93 ++++++++++++------- .../Management/ManagementSession.swift | 29 ++++-- 5 files changed, 163 insertions(+), 65 deletions(-) create mode 100644 YubiKit/YubiKit/Management/Capability.swift diff --git a/FullStackTests/Tests/ManagementFullStackTests.swift b/FullStackTests/Tests/ManagementFullStackTests.swift index 31de18b..b390032 100644 --- a/FullStackTests/Tests/ManagementFullStackTests.swift +++ b/FullStackTests/Tests/ManagementFullStackTests.swift @@ -33,10 +33,20 @@ class ManagementFullStackTests: XCTestCase { runManagementTest { connection, session, _ in let info = try await session.getDeviceInfo() print(info) - print("PIV enabled over usb: \(info.config.isApplicationEnabled(.piv, overTransport: .usb))") - print("PIV enabled over nfc: \(info.config.isApplicationEnabled(.piv, overTransport: .nfc))") - print("PIV supported over usb: \(info.isApplicationSupported(.piv, overTransport: .usb))") - print("PIV supported over nfc: \(info.isApplicationSupported(.piv, overTransport: .nfc))") + print("PIV enabled over usb: \(info.config.isApplicationEnabled(.PIV, overTransport: .usb))") + print("PIV enabled over nfc: \(info.config.isApplicationEnabled(.PIV, overTransport: .nfc))") + print("PIV supported over usb: \(info.isApplicationSupported(.PIV, overTransport: .usb))") + print("PIV supported over nfc: \(info.isApplicationSupported(.PIV, overTransport: .nfc))") + print("Is FIPS key: \(info.isFips)") + print("Is Sky key: \(info.isSky)") + print("Is config locked: \(info.isConfigLocked)") + print("Serial number: \(info.serialNumber)") + print("Part number: \(info.partNumber)") + print("Is stm version: \(info.stmVersion?.description ?? "n/a")") + print("Is fps version: \(info.fpsVersion?.description ?? "n/a")") + print("Is fips capable: \(info.isFIPSCapable)") + print("Is fips approved: \(info.isFIPSApproved)") + print("Pin complexity: \(info.pinComplexity)") #if os(iOS) await connection.nfcConnection?.close(message: "Test successful!") #endif @@ -60,65 +70,65 @@ class ManagementFullStackTests: XCTestCase { func testDisableAndEnableConfigOATHandPIVoverUSB() throws { runManagementTest { connection, session, transport in let deviceInfo = try await session.getDeviceInfo() - guard let disableConfig = deviceInfo.config.deviceConfig(enabling: false, application: .oath, overTransport: .usb)?.deviceConfig(enabling: false, application: .piv, overTransport: .usb) else { XCTFail(); return } + guard let disableConfig = deviceInfo.config.deviceConfig(enabling: false, application: .OATH, overTransport: .usb)?.deviceConfig(enabling: false, application: .PIV, overTransport: .usb) else { XCTFail(); return } try await session.updateDeviceConfig(disableConfig, reboot: false) let disabledInfo = try await session.getDeviceInfo() - XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.oath, overTransport: .usb)) - XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.piv, overTransport: .usb)) + XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.OATH, overTransport: .usb)) + XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.PIV, overTransport: .usb)) let oathSession = try? await OATHSession.session(withConnection: connection) if transport == .usb { XCTAssert(oathSession == nil) } let managementSession = try await ManagementSession.session(withConnection: connection) - guard let enableConfig = deviceInfo.config.deviceConfig(enabling: true, application: .oath, overTransport: .usb)?.deviceConfig(enabling: true, application: .piv, overTransport: .usb) else { XCTFail(); return } + guard let enableConfig = deviceInfo.config.deviceConfig(enabling: true, application: .OATH, overTransport: .usb)?.deviceConfig(enabling: true, application: .PIV, overTransport: .usb) else { XCTFail(); return } try await managementSession.updateDeviceConfig(enableConfig, reboot: false) #if os(iOS) await connection.nfcConnection?.close(message: "Test successful!") #endif let enabledInfo = try await managementSession.getDeviceInfo() - XCTAssert(enabledInfo.config.isApplicationEnabled(.oath, overTransport: .usb)) - XCTAssert(enabledInfo.config.isApplicationEnabled(.piv, overTransport: .usb)) + XCTAssert(enabledInfo.config.isApplicationEnabled(.OATH, overTransport: .usb)) + XCTAssert(enabledInfo.config.isApplicationEnabled(.PIV, overTransport: .usb)) } } func testDisableAndEnableConfigOATHandPIVoverNFC() throws { runManagementTest { connection, session, transport in let deviceInfo = try await session.getDeviceInfo() - guard let disableConfig = deviceInfo.config.deviceConfig(enabling: false, application: .oath, overTransport: .nfc)?.deviceConfig(enabling: false, application: .piv, overTransport: .nfc) else { XCTFail(); return } + guard let disableConfig = deviceInfo.config.deviceConfig(enabling: false, application: .OATH, overTransport: .nfc)?.deviceConfig(enabling: false, application: .PIV, overTransport: .nfc) else { XCTFail(); return } try await session.updateDeviceConfig(disableConfig, reboot: false) let disabledInfo = try await session.getDeviceInfo() - XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.oath, overTransport: .nfc)) - XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.piv, overTransport: .nfc)) + XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.OATH, overTransport: .nfc)) + XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.PIV, overTransport: .nfc)) let oathSession = try? await OATHSession.session(withConnection: connection) if transport == .nfc { XCTAssert(oathSession == nil) } let managementSession = try await ManagementSession.session(withConnection: connection) - guard let enableConfig = deviceInfo.config.deviceConfig(enabling: true, application: .oath, overTransport: .nfc)?.deviceConfig(enabling: true, application: .piv, overTransport: .nfc) else { XCTFail(); return } + guard let enableConfig = deviceInfo.config.deviceConfig(enabling: true, application: .OATH, overTransport: .nfc)?.deviceConfig(enabling: true, application: .PIV, overTransport: .nfc) else { XCTFail(); return } try await managementSession.updateDeviceConfig(enableConfig, reboot: false) #if os(iOS) await connection.nfcConnection?.close(message: "Test successful!") #endif let enabledInfo = try await managementSession.getDeviceInfo() - XCTAssert(enabledInfo.config.isApplicationEnabled(.oath, overTransport: .nfc)) - XCTAssert(enabledInfo.config.isApplicationEnabled(.piv, overTransport: .nfc)) + XCTAssert(enabledInfo.config.isApplicationEnabled(.OATH, overTransport: .nfc)) + XCTAssert(enabledInfo.config.isApplicationEnabled(.PIV, overTransport: .nfc)) } } func testDisableAndEnableWithHelperOATH() throws { runManagementTest { connection, session, transport in - try await session.setEnabled(false, application: .oath, overTransport: transport) + try await session.setEnabled(false, application: .OATH, overTransport: transport) var info = try await session.getDeviceInfo() - XCTAssertFalse(info.config.isApplicationEnabled(.oath, overTransport: transport)) + XCTAssertFalse(info.config.isApplicationEnabled(.OATH, overTransport: transport)) let oathSession = try? await OATHSession.session(withConnection: connection) XCTAssert(oathSession == nil) let managementSession = try await ManagementSession.session(withConnection: connection) - try await managementSession.setEnabled(true, application: .oath, overTransport: transport) + try await managementSession.setEnabled(true, application: .OATH, overTransport: transport) info = try await managementSession.getDeviceInfo() #if os(iOS) await connection.nfcConnection?.close(message: "Test successful!") #endif - XCTAssert(info.config.isApplicationEnabled(.oath, overTransport: transport)) + XCTAssert(info.config.isApplicationEnabled(.OATH, overTransport: transport)) } } } diff --git a/YubiKit/YubiKit/Management/Capability.swift b/YubiKit/YubiKit/Management/Capability.swift new file mode 100644 index 0000000..42705ed --- /dev/null +++ b/YubiKit/YubiKit/Management/Capability.swift @@ -0,0 +1,51 @@ +// +// Capability.swift +// YubiKit +// +// Created by Jens Utbult on 2024-05-17. +// + +import Foundation + +/// Identifies a feature (typically an application) on a YubiKey which may or may not be supported, and which can be enabled or disabled. +public enum Capability: UInt { + /// Identifies the YubiOTP application. + case OTP = 0x0001 + /// Identifies the U2F (CTAP1) portion of the FIDO application. + case U2F = 0x0002 + /// Identifies the OpenPGP application, implementing the OpenPGP Card protocol. + case OPENPGP = 0x0008 + /// Identifies the PIV application, implementing the PIV protocol. + case PIV = 0x0010 + /// Identifies the OATH application, implementing the YKOATH protocol. + case OATH = 0x0020 + /// Identifies the HSMAUTH application. + case HSMAUTH = 0x0100 + /// Identifies the FIDO2 (CTAP2) portion of the FIDO application. + case FIDO2 = 0x02000 + + var bit: UInt { self.rawValue } +} + + +extension Capability { + static func translateMaskFrom(fipsMask: UInt) -> UInt { + var capabilities: UInt = 0; + if fipsMask & 0b00000001 != 0 { + capabilities |= Capability.FIDO2.bit; + } + if fipsMask & 0b00000010 != 0 { + capabilities |= Capability.PIV.bit; + } + if fipsMask & 0b00000100 != 0 { + capabilities |= Capability.OPENPGP.bit; + } + if fipsMask & 0b00001000 != 0 { + capabilities |= Capability.OATH.bit; + } + if fipsMask & 0b00010000 != 0 { + capabilities |= Capability.HSMAUTH.bit; + } + return capabilities; + } +} diff --git a/YubiKit/YubiKit/Management/DeviceConfig.swift b/YubiKit/YubiKit/Management/DeviceConfig.swift index 6f67a4b..0bbddd8 100644 --- a/YubiKit/YubiKit/Management/DeviceConfig.swift +++ b/YubiKit/YubiKit/Management/DeviceConfig.swift @@ -31,13 +31,14 @@ public struct DeviceConfig { internal let tagConfigurationLock: TKTLVTag = 0x0a internal let tagUnlock: TKTLVTag = 0x0b internal let tagReboot: TKTLVTag = 0x0c + internal let tagNFCRestricted: TKTLVTag = 0x17 - public func isApplicationEnabled(_ application: ApplicationType, overTransport transport: DeviceTransport) -> Bool { + public func isApplicationEnabled(_ application: Capability, overTransport transport: DeviceTransport) -> Bool { guard let mask = enabledCapabilities[transport] else { return false } return (mask & application.rawValue) == application.rawValue } - public func deviceConfig(enabling: Bool, application: ApplicationType, overTransport transport: DeviceTransport) -> DeviceConfig? { + public func deviceConfig(enabling: Bool, application: Capability, overTransport transport: DeviceTransport) -> DeviceConfig? { guard let oldMask = enabledCapabilities[transport] else { return nil } let newMask = enabling ? oldMask | application.rawValue : oldMask & ~application.rawValue var newEnabledCapabilities = enabledCapabilities diff --git a/YubiKit/YubiKit/Management/DeviceInfo.swift b/YubiKit/YubiKit/Management/DeviceInfo.swift index 0317c32..f3473dc 100644 --- a/YubiKit/YubiKit/Management/DeviceInfo.swift +++ b/YubiKit/YubiKit/Management/DeviceInfo.swift @@ -20,22 +20,6 @@ public enum DeviceTransport { case usb, nfc } -/// Identifies a feature (typically an application) on a YubiKey which may or may not be supported, and which can be enabled or disabled. -public enum ApplicationType: UInt { - /// Identifies the YubiOTP application. - case otp = 0x01 - /// Identifies the U2F (CTAP1) portion of the FIDO application. - case u2f = 0x02 - /// Identifies the OpenPGP application, implementing the OpenPGP Card protocol. - case opgp = 0x08 - /// Identifies the PIV application, implementing the PIV protocol. - case piv = 0x10 - /// Identifies the OATH application, implementing the YKOATH protocol. - case oath = 0x20 - /// Identifies the FIDO2 (CTAP2) portion of the FIDO application. - case ctap2 = 0x0200 -} - /// The physical form factor of a YubiKey. public enum FormFactor: UInt8 { /// Used when information about the YubiKey's form factor isn't available. @@ -67,6 +51,17 @@ public struct DeviceInfo { public let version: Version /// Returns the form factor of the YubiKey. public let formFactor: FormFactor + + public let partNumber: String + + public let isFIPSCapable: UInt + + public let isFIPSApproved: UInt + + public let fpsVersion: Version? + + public let stmVersion: Version? + /// Returns the supported (not necessarily enabled) capabilities for a given transport. public let supportedCapabilities: [DeviceTransport: UInt] /// Returns whether or not a Configuration Lock is set for the Management application on the YubiKey. @@ -77,31 +72,50 @@ public struct DeviceInfo { public let isSky: Bool /// The mutable configuration of the YubiKey. public let config: DeviceConfig + /// PIN complexity + public let pinComplexity: Bool - internal let tagIsUSBSupported: TKTLVTag = 0x01 + public let isResetBlocked: UInt + + internal let tagUSBSupported: TKTLVTag = 0x01 internal let tagSerialNumber: TKTLVTag = 0x02 - internal let tagIsUSBEnabled: TKTLVTag = 0x03 + internal let tagUSBEnabled: TKTLVTag = 0x03 internal let tagFormFactor: TKTLVTag = 0x04 internal let tagFirmwareVersion: TKTLVTag = 0x05 internal let tagAutoEjectTimeout: TKTLVTag = 0x06 internal let tagChallengeResponseTimeout: TKTLVTag = 0x07 internal let tagDeviceFlags: TKTLVTag = 0x08 - internal let tagIsNFCSupported: TKTLVTag = 0x0d - internal let tagIsNFCEnabled: TKTLVTag = 0x0e - internal let tagIsConfigLocked: TKTLVTag = 0x0a + internal let tagNFCSupported: TKTLVTag = 0x0d + internal let tagNFCEnabled: TKTLVTag = 0x0e + internal let tagConfigLocked: TKTLVTag = 0x0a + internal let tagPartNumber: TKTLVTag = 0x13 + internal let tagFIPSCapable: TKTLVTag = 0x14 + internal let tagFIPSApproved: TKTLVTag = 0x15 + internal let tagPINComplexity: TKTLVTag = 0x16 + internal let tagNFCRestricted: TKTLVTag = 0x17 + internal let tagResetBlocked: TKTLVTag = 0x18 + internal let tagFPSVersion: TKTLVTag = 0x20 + internal let tagSTMVersion: TKTLVTag = 0x21 - internal init(withData data: Data, fallbackVersion: Version) throws { - guard let count = data.bytes.first, count > 0 else { throw ManagementSessionError.missingData } - guard let tlvs = TKBERTLVRecord.dictionaryOfData(from: data.subdata(in: 1.. Bool { + public func isApplicationSupported(_ application: Capability, overTransport transport: DeviceTransport) -> Bool { guard let mask = supportedCapabilities[transport] else { return false } return (mask & application.rawValue) == application.rawValue } diff --git a/YubiKit/YubiKit/Management/ManagementSession.swift b/YubiKit/YubiKit/Management/ManagementSession.swift index 9212ecd..6226ac8 100644 --- a/YubiKit/YubiKit/Management/ManagementSession.swift +++ b/YubiKit/YubiKit/Management/ManagementSession.swift @@ -82,9 +82,22 @@ public final actor ManagementSession: Session, InternalSession { Logger.management.debug("\(String(describing: self).lastComponent), \(#function)") guard self.supports(ManagementFeature.deviceInfo) else { throw SessionError.notSupported } guard let connection = _connection else { throw SessionError.noConnection } - let apdu = APDU(cla: 0, ins: 0x1d, p1: 0, p2: 0) - let data = try await connection.send(apdu: apdu) - return try DeviceInfo(withData: data, fallbackVersion: version) + + var page: UInt8 = 0 + var hasMoreData = true + var result = [TKTLVTag : Data]() + while hasMoreData { + let apdu = APDU(cla: 0, ins: 0x1d, p1: page, p2: 0) + let data = try await connection.send(apdu: apdu) + guard let count = data.bytes.first, count > 0 else { throw ManagementSessionError.missingData } + guard let tlvs = TKBERTLVRecord.dictionaryOfData(from: data.subdata(in: 1.. Bool { + public func isApplicationSupported(_ application: Capability, overTransport transport: DeviceTransport) async throws -> Bool { Logger.management.debug("\(String(describing: self).lastComponent), \(#function)") let deviceInfo = try await getDeviceInfo() return deviceInfo.isApplicationSupported(application, overTransport: transport) } /// Check whether an application is enabled over the specified transport. - public func isApplicationEnabled(_ application: ApplicationType, overTransport transport: DeviceTransport) async throws -> Bool { + public func isApplicationEnabled(_ application: Capability, overTransport transport: DeviceTransport) async throws -> Bool { Logger.management.debug("\(String(describing: self).lastComponent), \(#function)") let deviceInfo = try await getDeviceInfo() return deviceInfo.config.isApplicationEnabled(application, overTransport: transport) } /// Enable or disable an application over the specified transport. - public func setEnabled(_ enabled: Bool, application: ApplicationType, overTransport transport: DeviceTransport, reboot: Bool = false) async throws { + public func setEnabled(_ enabled: Bool, application: Capability, overTransport transport: DeviceTransport, reboot: Bool = false) async throws { Logger.management.debug("\(String(describing: self).lastComponent), \(#function): \(enabled), application: \(String(describing: application)), overTransport: \(String(describing: transport)), reboot: \(reboot)") let deviceInfo = try await getDeviceInfo() guard enabled != deviceInfo.config.isApplicationEnabled(application, overTransport: transport) else { return } @@ -128,13 +141,13 @@ public final actor ManagementSession: Session, InternalSession { } /// Disable an application over the specified transport. - public func disableApplication(_ application: ApplicationType, overTransport transport: DeviceTransport, reboot: Bool = false) async throws { + public func disableApplication(_ application: Capability, overTransport transport: DeviceTransport, reboot: Bool = false) async throws { Logger.management.debug("\(String(describing: self).lastComponent), \(#function): \(String(describing: application)), overTransport: \(String(describing: transport)), reboot: \(reboot)") try await setEnabled(false, application: application, overTransport: transport, reboot: reboot) } /// Enable an application over the specified transport. - public func enableApplication(_ application: ApplicationType, overTransport transport: DeviceTransport, reboot: Bool = false) async throws { + public func enableApplication(_ application: Capability, overTransport transport: DeviceTransport, reboot: Bool = false) async throws { Logger.management.debug("\(String(describing: self).lastComponent), \(#function): \(String(describing: application)), overTransport: \(String(describing: transport)), reboot: \(reboot)") try await setEnabled(true, application: application, overTransport: transport, reboot: reboot) } From 57dcf05115e04922404f7a25f04f02690e734679 Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Tue, 21 May 2024 10:42:41 +0200 Subject: [PATCH 2/7] Added support for reading pin complexity, nfc restriction, fps and stm version. Also enabling nfc restriction is now supported. --- .../Tests/ManagementFullStackTests.swift | 59 ++++++----- YubiKit/YubiKit.xcodeproj/project.pbxproj | 4 + YubiKit/YubiKit/Management/DeviceConfig.swift | 57 ++++++++++- YubiKit/YubiKit/Management/DeviceInfo.swift | 98 +++++++++---------- YubiKit/YubiKit/Version.swift | 3 +- 5 files changed, 144 insertions(+), 77 deletions(-) diff --git a/FullStackTests/Tests/ManagementFullStackTests.swift b/FullStackTests/Tests/ManagementFullStackTests.swift index b390032..9d62657 100644 --- a/FullStackTests/Tests/ManagementFullStackTests.swift +++ b/FullStackTests/Tests/ManagementFullStackTests.swift @@ -32,21 +32,7 @@ class ManagementFullStackTests: XCTestCase { func testGetDeviceInfo() throws { runManagementTest { connection, session, _ in let info = try await session.getDeviceInfo() - print(info) - print("PIV enabled over usb: \(info.config.isApplicationEnabled(.PIV, overTransport: .usb))") - print("PIV enabled over nfc: \(info.config.isApplicationEnabled(.PIV, overTransport: .nfc))") - print("PIV supported over usb: \(info.isApplicationSupported(.PIV, overTransport: .usb))") - print("PIV supported over nfc: \(info.isApplicationSupported(.PIV, overTransport: .nfc))") - print("Is FIPS key: \(info.isFips)") - print("Is Sky key: \(info.isSky)") - print("Is config locked: \(info.isConfigLocked)") - print("Serial number: \(info.serialNumber)") - print("Part number: \(info.partNumber)") - print("Is stm version: \(info.stmVersion?.description ?? "n/a")") - print("Is fps version: \(info.fpsVersion?.description ?? "n/a")") - print("Is fips capable: \(info.isFIPSCapable)") - print("Is fips approved: \(info.isFIPSApproved)") - print("Pin complexity: \(info.pinComplexity)") + print("✅ Successfully got device info:\n\(info)") #if os(iOS) await connection.nfcConnection?.close(message: "Test successful!") #endif @@ -82,18 +68,19 @@ class ManagementFullStackTests: XCTestCase { let managementSession = try await ManagementSession.session(withConnection: connection) guard let enableConfig = deviceInfo.config.deviceConfig(enabling: true, application: .OATH, overTransport: .usb)?.deviceConfig(enabling: true, application: .PIV, overTransport: .usb) else { XCTFail(); return } try await managementSession.updateDeviceConfig(enableConfig, reboot: false) - #if os(iOS) - await connection.nfcConnection?.close(message: "Test successful!") - #endif let enabledInfo = try await managementSession.getDeviceInfo() XCTAssert(enabledInfo.config.isApplicationEnabled(.OATH, overTransport: .usb)) XCTAssert(enabledInfo.config.isApplicationEnabled(.PIV, overTransport: .usb)) + #if os(iOS) + await connection.nfcConnection?.close(message: "Test successful!") + #endif } } func testDisableAndEnableConfigOATHandPIVoverNFC() throws { runManagementTest { connection, session, transport in let deviceInfo = try await session.getDeviceInfo() + guard deviceInfo.hasTransport(.nfc) else { print("⚠️ No NFC YubiKey. Skip test."); return } guard let disableConfig = deviceInfo.config.deviceConfig(enabling: false, application: .OATH, overTransport: .nfc)?.deviceConfig(enabling: false, application: .PIV, overTransport: .nfc) else { XCTFail(); return } try await session.updateDeviceConfig(disableConfig, reboot: false) let disabledInfo = try await session.getDeviceInfo() @@ -106,12 +93,12 @@ class ManagementFullStackTests: XCTestCase { let managementSession = try await ManagementSession.session(withConnection: connection) guard let enableConfig = deviceInfo.config.deviceConfig(enabling: true, application: .OATH, overTransport: .nfc)?.deviceConfig(enabling: true, application: .PIV, overTransport: .nfc) else { XCTFail(); return } try await managementSession.updateDeviceConfig(enableConfig, reboot: false) - #if os(iOS) - await connection.nfcConnection?.close(message: "Test successful!") - #endif let enabledInfo = try await managementSession.getDeviceInfo() XCTAssert(enabledInfo.config.isApplicationEnabled(.OATH, overTransport: .nfc)) XCTAssert(enabledInfo.config.isApplicationEnabled(.PIV, overTransport: .nfc)) + #if os(iOS) + await connection.nfcConnection?.close(message: "Test successful!") + #endif } } @@ -125,12 +112,40 @@ class ManagementFullStackTests: XCTestCase { let managementSession = try await ManagementSession.session(withConnection: connection) try await managementSession.setEnabled(true, application: .OATH, overTransport: transport) info = try await managementSession.getDeviceInfo() + XCTAssert(info.config.isApplicationEnabled(.OATH, overTransport: transport)) #if os(iOS) await connection.nfcConnection?.close(message: "Test successful!") #endif - XCTAssert(info.config.isApplicationEnabled(.OATH, overTransport: transport)) } } + + func testZNFCRestricted() throws { + runManagementTest { connection, session, transport in + guard session.version >= Version(withString: "5.7.0")! else { + print("⚠️ YubiKey without support for NFC restricted. Skip test.") + return + } + let info = try await session.getDeviceInfo() + let newConfig = info.config.deviceConfig(nfcRestricted: true) + try await session.updateDeviceConfig(newConfig, reboot: false) + let updatedInfo = try await session.getDeviceInfo() + XCTAssertEqual(updatedInfo.config.isNFCRestricted, true) + if transport == .nfc { + #if os(iOS) + await connection.nfcConnection?.close(message: "NFC is now restriced until this YubiKey has been inserted into a USB port.") + do { + let newConnection = try await ConnectionHelper.anyConnection() + _ = try await ManagementSession.session(withConnection: newConnection) + XCTFail("Got connection even if NFC restriced was turned on!") + } catch { + print("✅ Failed creating ManagementSession as expected.") + } + #endif + } + print("✅ NFC is now restriced until this YubiKey has been inserted into a USB port.") + print("⚠️ Note that no more NFC testing will be possible until NFC restriction has been disabled for this key!") + } + } } extension XCTestCase { diff --git a/YubiKit/YubiKit.xcodeproj/project.pbxproj b/YubiKit/YubiKit.xcodeproj/project.pbxproj index 9197f60..34bad01 100644 --- a/YubiKit/YubiKit.xcodeproj/project.pbxproj +++ b/YubiKit/YubiKit.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 51BBE3EB273D1A3800DA47CC /* YubiKit.docc in Sources */ = {isa = PBXBuildFile; fileRef = 51BBE3EA273D1A3800DA47CC /* YubiKit.docc */; }; 51BBE3F1273D1A3800DA47CC /* YubiKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51BBE3E6273D1A3800DA47CC /* YubiKit.framework */; }; 51BBE3F7273D1A3800DA47CC /* YubiKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 51BBE3E9273D1A3800DA47CC /* YubiKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B40064302BF728D600CD2FAF /* Capability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B400642F2BF728D600CD2FAF /* Capability.swift */; }; B401F7762B17B8DD00C541D1 /* Logger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B401F7752B17B8DD00C541D1 /* Logger+Extensions.swift */; }; B40528332987C31E00FC33AB /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40528322987C31E00FC33AB /* DeviceInfo.swift */; }; B405283729894E7600FC33AB /* DeviceConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = B405283629894E7600FC33AB /* DeviceConfig.swift */; }; @@ -64,6 +65,7 @@ 51BBE3E9273D1A3800DA47CC /* YubiKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YubiKit.h; sourceTree = ""; }; 51BBE3EA273D1A3800DA47CC /* YubiKit.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = YubiKit.docc; sourceTree = ""; }; 51BBE3F0273D1A3800DA47CC /* YubiKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = YubiKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B400642F2BF728D600CD2FAF /* Capability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Capability.swift; sourceTree = ""; }; B401F7752B17B8DD00C541D1 /* Logger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Extensions.swift"; sourceTree = ""; }; B40528322987C31E00FC33AB /* DeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = ""; }; B405283629894E7600FC33AB /* DeviceConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceConfig.swift; sourceTree = ""; }; @@ -208,6 +210,7 @@ B40528322987C31E00FC33AB /* DeviceInfo.swift */, B405283629894E7600FC33AB /* DeviceConfig.swift */, B49F90C32B9F30A400C10F0B /* ManagementFeature.swift */, + B400642F2BF728D600CD2FAF /* Capability.swift */, ); path = Management; sourceTree = ""; @@ -389,6 +392,7 @@ B41B61842743FC2E004C37BF /* Connection.swift in Sources */, B4BE3AAD292E1C8600CC30CB /* OATHSession.swift in Sources */, B4BE3ABA292E24C700CC30CB /* Base32.swift in Sources */, + B40064302BF728D600CD2FAF /* Capability.swift in Sources */, B4F068B92861CFC300555AFE /* SmartCardConnection.swift in Sources */, B4F937622B51A44E0007D394 /* PIVSession.swift in Sources */, B40CBD6029090C49007D7D23 /* Version.swift in Sources */, diff --git a/YubiKit/YubiKit/Management/DeviceConfig.swift b/YubiKit/YubiKit/Management/DeviceConfig.swift index 0bbddd8..1b7fcff 100644 --- a/YubiKit/YubiKit/Management/DeviceConfig.swift +++ b/YubiKit/YubiKit/Management/DeviceConfig.swift @@ -22,22 +22,65 @@ public struct DeviceConfig { public let challengeResponseTimeout: TimeInterval? public let deviceFlags: UInt8? public let enabledCapabilities: [DeviceTransport: UInt] + public let isNFCRestricted: Bool? internal let tagUSBEnabled: TKTLVTag = 0x03 internal let tagAutoEjectTimeout: TKTLVTag = 0x06 internal let tagChallengeResponseTimeout: TKTLVTag = 0x07 internal let tagDeviceFlags: TKTLVTag = 0x08 + internal let tagNFCSupported: TKTLVTag = 0x0d internal let tagNFCEnabled: TKTLVTag = 0x0e internal let tagConfigurationLock: TKTLVTag = 0x0a internal let tagUnlock: TKTLVTag = 0x0b internal let tagReboot: TKTLVTag = 0x0c internal let tagNFCRestricted: TKTLVTag = 0x17 + internal init(withTlvs tlvs: [TKTLVTag : Data], version: Version) throws { + if let timeout = tlvs[tagAutoEjectTimeout]?.integer { + self.autoEjectTimeout = TimeInterval(timeout) + } else { + self.autoEjectTimeout = 0 + } + + if let timeout = tlvs[tagChallengeResponseTimeout]?.integer { + self.challengeResponseTimeout = TimeInterval(timeout) + } else { + self.challengeResponseTimeout = 0 + } + + self.deviceFlags = tlvs[tagDeviceFlags]?.uint8 + + var enabledCapabilities = [DeviceTransport: UInt]() + if tlvs[tagUSBEnabled] != nil && version.major != 4 { + // YK4 reports this incorrectly, instead use supportedCapabilities and USB mode. + enabledCapabilities[DeviceTransport.usb] = tlvs[tagUSBEnabled]?.integer ?? 0 + } + + if tlvs[tagNFCSupported] != nil { + enabledCapabilities[DeviceTransport.nfc] = tlvs[tagNFCEnabled]?.integer ?? 0 + } + self.enabledCapabilities = enabledCapabilities + if let isNFCRestricted = tlvs[tagNFCRestricted]?.integer { + self.isNFCRestricted = isNFCRestricted == 1 + } else { + self.isNFCRestricted = nil + } + } + public func isApplicationEnabled(_ application: Capability, overTransport transport: DeviceTransport) -> Bool { guard let mask = enabledCapabilities[transport] else { return false } return (mask & application.rawValue) == application.rawValue } + + private init(autoEjectTimeout: TimeInterval?, challengeResponseTimeout: TimeInterval?, deviceFlags: UInt8?, enabledCapabilities: [DeviceTransport : UInt], isNFCRestricted: Bool?) { + self.autoEjectTimeout = autoEjectTimeout + self.challengeResponseTimeout = challengeResponseTimeout + self.deviceFlags = deviceFlags + self.enabledCapabilities = enabledCapabilities + self.isNFCRestricted = isNFCRestricted + } + public func deviceConfig(enabling: Bool, application: Capability, overTransport transport: DeviceTransport) -> DeviceConfig? { guard let oldMask = enabledCapabilities[transport] else { return nil } let newMask = enabling ? oldMask | application.rawValue : oldMask & ~application.rawValue @@ -47,11 +90,16 @@ public struct DeviceConfig { return DeviceConfig(autoEjectTimeout: autoEjectTimeout, challengeResponseTimeout: challengeResponseTimeout, deviceFlags: deviceFlags, - enabledCapabilities: newEnabledCapabilities) + enabledCapabilities: newEnabledCapabilities, + isNFCRestricted: self.isNFCRestricted) } public func deviceConfig(autoEjectTimeout: TimeInterval, challengeResponseTimeout: TimeInterval) -> DeviceConfig { - return Self.init(autoEjectTimeout: autoEjectTimeout, challengeResponseTimeout: challengeResponseTimeout, deviceFlags: self.deviceFlags, enabledCapabilities: self.enabledCapabilities) + return Self.init(autoEjectTimeout: autoEjectTimeout, challengeResponseTimeout: challengeResponseTimeout, deviceFlags: self.deviceFlags, enabledCapabilities: self.enabledCapabilities, isNFCRestricted: self.isNFCRestricted) + } + + public func deviceConfig(nfcRestricted: Bool) -> DeviceConfig { + return Self.init(autoEjectTimeout: self.autoEjectTimeout, challengeResponseTimeout: self.challengeResponseTimeout, deviceFlags: self.deviceFlags, enabledCapabilities: self.enabledCapabilities, isNFCRestricted: nfcRestricted) } internal func data(reboot: Bool, lockCode: Data?, newLockCode: Data?) throws -> Data { @@ -81,6 +129,11 @@ public struct DeviceConfig { if let newLockCode { data.append(TKBERTLVRecord(tag: tagConfigurationLock, value: newLockCode).data) } + + if let isNFCRestricted, isNFCRestricted { + data.append(TKBERTLVRecord(tag: tagNFCRestricted, value: UInt8(0x01).data).data) + } + guard data.count <= 0xff else { throw ManagementSessionError.configTooLarge } return UInt8(data.count).data + data diff --git a/YubiKit/YubiKit/Management/DeviceInfo.swift b/YubiKit/YubiKit/Management/DeviceInfo.swift index f3473dc..47f2ecf 100644 --- a/YubiKit/YubiKit/Management/DeviceInfo.swift +++ b/YubiKit/YubiKit/Management/DeviceInfo.swift @@ -41,7 +41,26 @@ public enum FormFactor: UInt8 { } /// Contains metadata, including Device Configuration, of a YubiKey. -public struct DeviceInfo { +public struct DeviceInfo: CustomStringConvertible { + + public var description: String { +""" +YubiKey \(formFactor) \(version) (#\(serialNumber)) +Supported capabilities: \(supportedCapabilities) +Enabled capabilities: \(config.enabledCapabilities) +isConfigLocked: \(isConfigLocked) +isFips: \(isFips) +isSky: \(isSky) +partNumber: \(partNumber) +isFipsCapable: \(isFIPSCapable) +isFipsApproved: \(isFIPSApproved) +pinComplexity: \(pinComplexity) +resetBlocked: \(isResetBlocked) +fpsVersion: \(String(describing: fpsVersion)) +stmVersion: \(String(describing: stmVersion)) +""" + } + /// Returns the serial number of the YubiKey, if available. /// /// The serial number can be read if the YubiKey has a serial number, and one of the YubiOTP slots @@ -99,24 +118,7 @@ public struct DeviceInfo { internal init(withTlvs tlvs: [TKTLVTag : Data], fallbackVersion: Version) throws { - if let data = tlvs[tagFirmwareVersion], let version = Version(withData: data) { - self.version = version - } else { - self.version = fallbackVersion - } - if let data = tlvs[tagFPSVersion], let version = Version(withData: data), version.description != "0.0.0" { - self.fpsVersion = version - } else { - self.fpsVersion = nil - } - if let data = tlvs[tagSTMVersion], let version = Version(withData: data), version.description != "0.0.0" { - self.stmVersion = version - } else { - self.stmVersion = nil - } - self.isConfigLocked = tlvs[tagConfigLocked]?.integer == 1 - self.serialNumber = tlvs[tagSerialNumber]?.integer ?? 0 if let rawFormFactor = tlvs[tagFormFactor]?.uint8 { @@ -133,53 +135,45 @@ public struct DeviceInfo { self.isSky = false } - self.partNumber = tlvs[tagPartNumber]?.stringUTF8 ?? "" - self.isFIPSCapable = Capability.translateMaskFrom(fipsMask: tlvs[tagFIPSCapable]?.integer ?? 0) self.isFIPSApproved = Capability.translateMaskFrom(fipsMask: tlvs[tagFIPSCapable]?.integer ?? 0) + self.pinComplexity = tlvs[tagPINComplexity]?.integer == 1 + + self.isResetBlocked = tlvs[tagResetBlocked]?.integer ?? 0 + + if let data = tlvs[tagFirmwareVersion], let version = Version(withData: data) { + self.version = version + } else { + self.version = fallbackVersion + } + if let data = tlvs[tagFPSVersion], let version = Version(withData: data), version.description != "0.0.0" { + self.fpsVersion = version + } else { + self.fpsVersion = nil + } + if let data = tlvs[tagSTMVersion], let version = Version(withData: data), version.description != "0.0.0" { + self.stmVersion = version + } else { + self.stmVersion = nil + } + + self.partNumber = tlvs[tagPartNumber]?.stringUTF8 ?? "" + var supportedCapabilities = [DeviceTransport: UInt]() if (version.major == 4 && version.minor == 2 && version.micro == 4) { - // 4.2.4 doesn't report supported capabilities correctly, but they are always 0x3f. + // 4.2.4 doesn't report supported capabilities correctly, but they are always 0x3f. supportedCapabilities[DeviceTransport.usb] = 0x3f - } else { + } else { supportedCapabilities[DeviceTransport.usb] = tlvs[tagUSBSupported]?.integer ?? 0 - } - - var enabledCapabilities = [DeviceTransport: UInt]() - if tlvs[tagUSBEnabled] != nil && version.major != 4 { - // YK4 reports this incorrectly, instead use supportedCapabilities and USB mode. - enabledCapabilities[DeviceTransport.usb] = tlvs[tagUSBEnabled]?.integer ?? 0 - } + } if let nfcSupported = tlvs[tagNFCSupported]?.integer { supportedCapabilities[DeviceTransport.nfc] = nfcSupported - enabledCapabilities[DeviceTransport.nfc] = tlvs[tagNFCEnabled]?.integer ?? 0 } self.supportedCapabilities = supportedCapabilities - // DeviceConfig - let autoEjectTimeout: TimeInterval - if let timeout = tlvs[tagAutoEjectTimeout]?.integer { - autoEjectTimeout = TimeInterval(timeout) - } else { - autoEjectTimeout = 0 - } - - let challengeResponseTimeout: TimeInterval - if let timeout = tlvs[tagChallengeResponseTimeout]?.integer { - challengeResponseTimeout = TimeInterval(timeout) - } else { - challengeResponseTimeout = 0 - } - - let deviceFlags = UInt8(tlvs[tagDeviceFlags]?.integer ?? 0) - - self.pinComplexity = tlvs[tagPINComplexity]?.integer == 1 - - self.isResetBlocked = tlvs[tagResetBlocked]?.integer ?? 0 - - self.config = DeviceConfig(autoEjectTimeout: autoEjectTimeout, challengeResponseTimeout: challengeResponseTimeout, deviceFlags: deviceFlags, enabledCapabilities: enabledCapabilities) + self.config = try DeviceConfig(withTlvs: tlvs, version: self.version) } /// Returns whether or not a specific transport is available on this YubiKey. diff --git a/YubiKit/YubiKit/Version.swift b/YubiKit/YubiKit/Version.swift index 0efb40f..e11a3b5 100644 --- a/YubiKit/YubiKit/Version.swift +++ b/YubiKit/YubiKit/Version.swift @@ -29,7 +29,8 @@ public struct Version: Comparable, CustomStringConvertible { micro = bytes[2] } - internal init?(withString string: String) { + /// Create a new Version from a version string, e.g. "5.7.0". + public init?(withString string: String) { let components = string.components(separatedBy: ".") guard components.count == 3, let major = UInt8(components[0]), From 5f18bd0f8115e750e88bfc669e5a3515978bfdb5 Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Thu, 23 May 2024 09:21:29 +0200 Subject: [PATCH 3/7] Fixed bug where we read the fips capable flag into the fips approved property. --- YubiKit/YubiKit/Management/DeviceInfo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YubiKit/YubiKit/Management/DeviceInfo.swift b/YubiKit/YubiKit/Management/DeviceInfo.swift index 47f2ecf..2828380 100644 --- a/YubiKit/YubiKit/Management/DeviceInfo.swift +++ b/YubiKit/YubiKit/Management/DeviceInfo.swift @@ -136,7 +136,7 @@ stmVersion: \(String(describing: stmVersion)) } self.isFIPSCapable = Capability.translateMaskFrom(fipsMask: tlvs[tagFIPSCapable]?.integer ?? 0) - self.isFIPSApproved = Capability.translateMaskFrom(fipsMask: tlvs[tagFIPSCapable]?.integer ?? 0) + self.isFIPSApproved = Capability.translateMaskFrom(fipsMask: tlvs[tagFIPSApproved]?.integer ?? 0) self.pinComplexity = tlvs[tagPINComplexity]?.integer == 1 From 3c05201e8ebe904af5d258461baa716ac7315088 Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Thu, 23 May 2024 09:45:45 +0200 Subject: [PATCH 4/7] Make mask translation for FIPS internal. --- YubiKit/YubiKit/Management/Capability.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YubiKit/YubiKit/Management/Capability.swift b/YubiKit/YubiKit/Management/Capability.swift index 42705ed..9051bce 100644 --- a/YubiKit/YubiKit/Management/Capability.swift +++ b/YubiKit/YubiKit/Management/Capability.swift @@ -29,7 +29,7 @@ public enum Capability: UInt { extension Capability { - static func translateMaskFrom(fipsMask: UInt) -> UInt { + internal static func translateMaskFrom(fipsMask: UInt) -> UInt { var capabilities: UInt = 0; if fipsMask & 0b00000001 != 0 { capabilities |= Capability.FIDO2.bit; From e4251de8b12a43a87290697f97e47a1400a12987 Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Thu, 23 May 2024 09:50:15 +0200 Subject: [PATCH 5/7] Added comment explaining why the test enabling nfc restriction has a z in the function name. --- FullStackTests/Tests/ManagementFullStackTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/FullStackTests/Tests/ManagementFullStackTests.swift b/FullStackTests/Tests/ManagementFullStackTests.swift index 9d62657..341466f 100644 --- a/FullStackTests/Tests/ManagementFullStackTests.swift +++ b/FullStackTests/Tests/ManagementFullStackTests.swift @@ -119,6 +119,7 @@ class ManagementFullStackTests: XCTestCase { } } + // Tests are run in alphabetical order. If running the tests via NFC this will disable NFC for all the following tests making them fail, hence the Z in the name. func testZNFCRestricted() throws { runManagementTest { connection, session, transport in guard session.version >= Version(withString: "5.7.0")! else { From 1458420447a879c632e41aff1953f9949bc01d13 Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Fri, 24 May 2024 09:14:21 +0200 Subject: [PATCH 6/7] Change fallback value of partNumber to be nil if it is parsed as an empty string or not present. --- YubiKit/YubiKit/Management/DeviceInfo.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/YubiKit/YubiKit/Management/DeviceInfo.swift b/YubiKit/YubiKit/Management/DeviceInfo.swift index 2828380..616b301 100644 --- a/YubiKit/YubiKit/Management/DeviceInfo.swift +++ b/YubiKit/YubiKit/Management/DeviceInfo.swift @@ -71,7 +71,7 @@ stmVersion: \(String(describing: stmVersion)) /// Returns the form factor of the YubiKey. public let formFactor: FormFactor - public let partNumber: String + public let partNumber: String? public let isFIPSCapable: UInt @@ -158,7 +158,7 @@ stmVersion: \(String(describing: stmVersion)) self.stmVersion = nil } - self.partNumber = tlvs[tagPartNumber]?.stringUTF8 ?? "" + self.partNumber = tlvs[tagPartNumber]?.stringUTF8.flatMap { $0.isEmpty ? nil : $0 } var supportedCapabilities = [DeviceTransport: UInt]() if (version.major == 4 && version.minor == 2 && version.micro == 4) { From a6581e4bf4cc8050d4cb35edbf2700ebcf18d8f1 Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Fri, 24 May 2024 09:20:13 +0200 Subject: [PATCH 7/7] FIDO2 got the wrong bitmask. --- YubiKit/YubiKit/Management/Capability.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/YubiKit/YubiKit/Management/Capability.swift b/YubiKit/YubiKit/Management/Capability.swift index 9051bce..132c37c 100644 --- a/YubiKit/YubiKit/Management/Capability.swift +++ b/YubiKit/YubiKit/Management/Capability.swift @@ -10,19 +10,19 @@ import Foundation /// Identifies a feature (typically an application) on a YubiKey which may or may not be supported, and which can be enabled or disabled. public enum Capability: UInt { /// Identifies the YubiOTP application. - case OTP = 0x0001 + case OTP = 0x0001 /// Identifies the U2F (CTAP1) portion of the FIDO application. - case U2F = 0x0002 + case U2F = 0x0002 /// Identifies the OpenPGP application, implementing the OpenPGP Card protocol. case OPENPGP = 0x0008 /// Identifies the PIV application, implementing the PIV protocol. - case PIV = 0x0010 + case PIV = 0x0010 /// Identifies the OATH application, implementing the YKOATH protocol. - case OATH = 0x0020 + case OATH = 0x0020 /// Identifies the HSMAUTH application. case HSMAUTH = 0x0100 /// Identifies the FIDO2 (CTAP2) portion of the FIDO application. - case FIDO2 = 0x02000 + case FIDO2 = 0x0200 var bit: UInt { self.rawValue } }