Skip to content

Commit

Permalink
Merge branch 'jens/management-new-features'
Browse files Browse the repository at this point in the history
  • Loading branch information
jensutbult committed May 28, 2024
2 parents 4666a2b + a6581e4 commit a7176f3
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 101 deletions.
72 changes: 49 additions & 23 deletions FullStackTests/Tests/ManagementFullStackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +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("✅ Successfully got device info:\n\(info)")
#if os(iOS)
await connection.nfcConnection?.close(message: "Test successful!")
#endif
Expand All @@ -60,67 +56,97 @@ 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)
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
let enabledInfo = try await managementSession.getDeviceInfo()
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 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()
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)
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
let enabledInfo = try await managementSession.getDeviceInfo()
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()
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))
}
}

// 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 {
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 {
Expand Down
4 changes: 4 additions & 0 deletions YubiKit/YubiKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -64,6 +65,7 @@
51BBE3E9273D1A3800DA47CC /* YubiKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YubiKit.h; sourceTree = "<group>"; };
51BBE3EA273D1A3800DA47CC /* YubiKit.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = YubiKit.docc; sourceTree = "<group>"; };
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 = "<group>"; };
B401F7752B17B8DD00C541D1 /* Logger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Extensions.swift"; sourceTree = "<group>"; };
B40528322987C31E00FC33AB /* DeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = "<group>"; };
B405283629894E7600FC33AB /* DeviceConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceConfig.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -208,6 +210,7 @@
B40528322987C31E00FC33AB /* DeviceInfo.swift */,
B405283629894E7600FC33AB /* DeviceConfig.swift */,
B49F90C32B9F30A400C10F0B /* ManagementFeature.swift */,
B400642F2BF728D600CD2FAF /* Capability.swift */,
);
path = Management;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
51 changes: 51 additions & 0 deletions YubiKit/YubiKit/Management/Capability.swift
Original file line number Diff line number Diff line change
@@ -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 = 0x0200

var bit: UInt { self.rawValue }
}


extension Capability {
internal 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;
}
}
62 changes: 58 additions & 4 deletions YubiKit/YubiKit/Management/DeviceConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,66 @@ 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

public func isApplicationEnabled(_ application: ApplicationType, overTransport transport: DeviceTransport) -> Bool {
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
}

public func deviceConfig(enabling: Bool, application: ApplicationType, overTransport transport: DeviceTransport) -> DeviceConfig? {

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
var newEnabledCapabilities = enabledCapabilities
Expand All @@ -46,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 {
Expand Down Expand Up @@ -80,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
Expand Down
Loading

0 comments on commit a7176f3

Please sign in to comment.