Skip to content

Commit a7176f3

Browse files
committed
Merge branch 'jens/management-new-features'
2 parents 4666a2b + a6581e4 commit a7176f3

File tree

7 files changed

+267
-101
lines changed

7 files changed

+267
-101
lines changed

FullStackTests/Tests/ManagementFullStackTests.swift

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,7 @@ class ManagementFullStackTests: XCTestCase {
3232
func testGetDeviceInfo() throws {
3333
runManagementTest { connection, session, _ in
3434
let info = try await session.getDeviceInfo()
35-
print(info)
36-
print("PIV enabled over usb: \(info.config.isApplicationEnabled(.piv, overTransport: .usb))")
37-
print("PIV enabled over nfc: \(info.config.isApplicationEnabled(.piv, overTransport: .nfc))")
38-
print("PIV supported over usb: \(info.isApplicationSupported(.piv, overTransport: .usb))")
39-
print("PIV supported over nfc: \(info.isApplicationSupported(.piv, overTransport: .nfc))")
35+
print("✅ Successfully got device info:\n\(info)")
4036
#if os(iOS)
4137
await connection.nfcConnection?.close(message: "Test successful!")
4238
#endif
@@ -60,67 +56,97 @@ class ManagementFullStackTests: XCTestCase {
6056
func testDisableAndEnableConfigOATHandPIVoverUSB() throws {
6157
runManagementTest { connection, session, transport in
6258
let deviceInfo = try await session.getDeviceInfo()
63-
guard let disableConfig = deviceInfo.config.deviceConfig(enabling: false, application: .oath, overTransport: .usb)?.deviceConfig(enabling: false, application: .piv, overTransport: .usb) else { XCTFail(); return }
59+
guard let disableConfig = deviceInfo.config.deviceConfig(enabling: false, application: .OATH, overTransport: .usb)?.deviceConfig(enabling: false, application: .PIV, overTransport: .usb) else { XCTFail(); return }
6460
try await session.updateDeviceConfig(disableConfig, reboot: false)
6561
let disabledInfo = try await session.getDeviceInfo()
66-
XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.oath, overTransport: .usb))
67-
XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.piv, overTransport: .usb))
62+
XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.OATH, overTransport: .usb))
63+
XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.PIV, overTransport: .usb))
6864
let oathSession = try? await OATHSession.session(withConnection: connection)
6965
if transport == .usb {
7066
XCTAssert(oathSession == nil)
7167
}
7268
let managementSession = try await ManagementSession.session(withConnection: connection)
73-
guard let enableConfig = deviceInfo.config.deviceConfig(enabling: true, application: .oath, overTransport: .usb)?.deviceConfig(enabling: true, application: .piv, overTransport: .usb) else { XCTFail(); return }
69+
guard let enableConfig = deviceInfo.config.deviceConfig(enabling: true, application: .OATH, overTransport: .usb)?.deviceConfig(enabling: true, application: .PIV, overTransport: .usb) else { XCTFail(); return }
7470
try await managementSession.updateDeviceConfig(enableConfig, reboot: false)
71+
let enabledInfo = try await managementSession.getDeviceInfo()
72+
XCTAssert(enabledInfo.config.isApplicationEnabled(.OATH, overTransport: .usb))
73+
XCTAssert(enabledInfo.config.isApplicationEnabled(.PIV, overTransport: .usb))
7574
#if os(iOS)
7675
await connection.nfcConnection?.close(message: "Test successful!")
7776
#endif
78-
let enabledInfo = try await managementSession.getDeviceInfo()
79-
XCTAssert(enabledInfo.config.isApplicationEnabled(.oath, overTransport: .usb))
80-
XCTAssert(enabledInfo.config.isApplicationEnabled(.piv, overTransport: .usb))
8177
}
8278
}
8379

8480
func testDisableAndEnableConfigOATHandPIVoverNFC() throws {
8581
runManagementTest { connection, session, transport in
8682
let deviceInfo = try await session.getDeviceInfo()
87-
guard let disableConfig = deviceInfo.config.deviceConfig(enabling: false, application: .oath, overTransport: .nfc)?.deviceConfig(enabling: false, application: .piv, overTransport: .nfc) else { XCTFail(); return }
83+
guard deviceInfo.hasTransport(.nfc) else { print("⚠️ No NFC YubiKey. Skip test."); return }
84+
guard let disableConfig = deviceInfo.config.deviceConfig(enabling: false, application: .OATH, overTransport: .nfc)?.deviceConfig(enabling: false, application: .PIV, overTransport: .nfc) else { XCTFail(); return }
8885
try await session.updateDeviceConfig(disableConfig, reboot: false)
8986
let disabledInfo = try await session.getDeviceInfo()
90-
XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.oath, overTransport: .nfc))
91-
XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.piv, overTransport: .nfc))
87+
XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.OATH, overTransport: .nfc))
88+
XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.PIV, overTransport: .nfc))
9289
let oathSession = try? await OATHSession.session(withConnection: connection)
9390
if transport == .nfc {
9491
XCTAssert(oathSession == nil)
9592
}
9693
let managementSession = try await ManagementSession.session(withConnection: connection)
97-
guard let enableConfig = deviceInfo.config.deviceConfig(enabling: true, application: .oath, overTransport: .nfc)?.deviceConfig(enabling: true, application: .piv, overTransport: .nfc) else { XCTFail(); return }
94+
guard let enableConfig = deviceInfo.config.deviceConfig(enabling: true, application: .OATH, overTransport: .nfc)?.deviceConfig(enabling: true, application: .PIV, overTransport: .nfc) else { XCTFail(); return }
9895
try await managementSession.updateDeviceConfig(enableConfig, reboot: false)
96+
let enabledInfo = try await managementSession.getDeviceInfo()
97+
XCTAssert(enabledInfo.config.isApplicationEnabled(.OATH, overTransport: .nfc))
98+
XCTAssert(enabledInfo.config.isApplicationEnabled(.PIV, overTransport: .nfc))
9999
#if os(iOS)
100100
await connection.nfcConnection?.close(message: "Test successful!")
101101
#endif
102-
let enabledInfo = try await managementSession.getDeviceInfo()
103-
XCTAssert(enabledInfo.config.isApplicationEnabled(.oath, overTransport: .nfc))
104-
XCTAssert(enabledInfo.config.isApplicationEnabled(.piv, overTransport: .nfc))
105102
}
106103
}
107104

108105
func testDisableAndEnableWithHelperOATH() throws {
109106
runManagementTest { connection, session, transport in
110-
try await session.setEnabled(false, application: .oath, overTransport: transport)
107+
try await session.setEnabled(false, application: .OATH, overTransport: transport)
111108
var info = try await session.getDeviceInfo()
112-
XCTAssertFalse(info.config.isApplicationEnabled(.oath, overTransport: transport))
109+
XCTAssertFalse(info.config.isApplicationEnabled(.OATH, overTransport: transport))
113110
let oathSession = try? await OATHSession.session(withConnection: connection)
114111
XCTAssert(oathSession == nil)
115112
let managementSession = try await ManagementSession.session(withConnection: connection)
116-
try await managementSession.setEnabled(true, application: .oath, overTransport: transport)
113+
try await managementSession.setEnabled(true, application: .OATH, overTransport: transport)
117114
info = try await managementSession.getDeviceInfo()
115+
XCTAssert(info.config.isApplicationEnabled(.OATH, overTransport: transport))
118116
#if os(iOS)
119117
await connection.nfcConnection?.close(message: "Test successful!")
120118
#endif
121-
XCTAssert(info.config.isApplicationEnabled(.oath, overTransport: transport))
122119
}
123120
}
121+
122+
// 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.
123+
func testZNFCRestricted() throws {
124+
runManagementTest { connection, session, transport in
125+
guard session.version >= Version(withString: "5.7.0")! else {
126+
print("⚠️ YubiKey without support for NFC restricted. Skip test.")
127+
return
128+
}
129+
let info = try await session.getDeviceInfo()
130+
let newConfig = info.config.deviceConfig(nfcRestricted: true)
131+
try await session.updateDeviceConfig(newConfig, reboot: false)
132+
let updatedInfo = try await session.getDeviceInfo()
133+
XCTAssertEqual(updatedInfo.config.isNFCRestricted, true)
134+
if transport == .nfc {
135+
#if os(iOS)
136+
await connection.nfcConnection?.close(message: "NFC is now restriced until this YubiKey has been inserted into a USB port.")
137+
do {
138+
let newConnection = try await ConnectionHelper.anyConnection()
139+
_ = try await ManagementSession.session(withConnection: newConnection)
140+
XCTFail("Got connection even if NFC restriced was turned on!")
141+
} catch {
142+
print("✅ Failed creating ManagementSession as expected.")
143+
}
144+
#endif
145+
}
146+
print("✅ NFC is now restriced until this YubiKey has been inserted into a USB port.")
147+
print("⚠️ Note that no more NFC testing will be possible until NFC restriction has been disabled for this key!")
148+
}
149+
}
124150
}
125151

126152
extension XCTestCase {

YubiKit/YubiKit.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
51BBE3EB273D1A3800DA47CC /* YubiKit.docc in Sources */ = {isa = PBXBuildFile; fileRef = 51BBE3EA273D1A3800DA47CC /* YubiKit.docc */; };
1111
51BBE3F1273D1A3800DA47CC /* YubiKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51BBE3E6273D1A3800DA47CC /* YubiKit.framework */; };
1212
51BBE3F7273D1A3800DA47CC /* YubiKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 51BBE3E9273D1A3800DA47CC /* YubiKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
13+
B40064302BF728D600CD2FAF /* Capability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B400642F2BF728D600CD2FAF /* Capability.swift */; };
1314
B401F7762B17B8DD00C541D1 /* Logger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B401F7752B17B8DD00C541D1 /* Logger+Extensions.swift */; };
1415
B40528332987C31E00FC33AB /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40528322987C31E00FC33AB /* DeviceInfo.swift */; };
1516
B405283729894E7600FC33AB /* DeviceConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = B405283629894E7600FC33AB /* DeviceConfig.swift */; };
@@ -64,6 +65,7 @@
6465
51BBE3E9273D1A3800DA47CC /* YubiKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YubiKit.h; sourceTree = "<group>"; };
6566
51BBE3EA273D1A3800DA47CC /* YubiKit.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = YubiKit.docc; sourceTree = "<group>"; };
6667
51BBE3F0273D1A3800DA47CC /* YubiKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = YubiKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
68+
B400642F2BF728D600CD2FAF /* Capability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Capability.swift; sourceTree = "<group>"; };
6769
B401F7752B17B8DD00C541D1 /* Logger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Extensions.swift"; sourceTree = "<group>"; };
6870
B40528322987C31E00FC33AB /* DeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = "<group>"; };
6971
B405283629894E7600FC33AB /* DeviceConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceConfig.swift; sourceTree = "<group>"; };
@@ -208,6 +210,7 @@
208210
B40528322987C31E00FC33AB /* DeviceInfo.swift */,
209211
B405283629894E7600FC33AB /* DeviceConfig.swift */,
210212
B49F90C32B9F30A400C10F0B /* ManagementFeature.swift */,
213+
B400642F2BF728D600CD2FAF /* Capability.swift */,
211214
);
212215
path = Management;
213216
sourceTree = "<group>";
@@ -389,6 +392,7 @@
389392
B41B61842743FC2E004C37BF /* Connection.swift in Sources */,
390393
B4BE3AAD292E1C8600CC30CB /* OATHSession.swift in Sources */,
391394
B4BE3ABA292E24C700CC30CB /* Base32.swift in Sources */,
395+
B40064302BF728D600CD2FAF /* Capability.swift in Sources */,
392396
B4F068B92861CFC300555AFE /* SmartCardConnection.swift in Sources */,
393397
B4F937622B51A44E0007D394 /* PIVSession.swift in Sources */,
394398
B40CBD6029090C49007D7D23 /* Version.swift in Sources */,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// Capability.swift
3+
// YubiKit
4+
//
5+
// Created by Jens Utbult on 2024-05-17.
6+
//
7+
8+
import Foundation
9+
10+
/// Identifies a feature (typically an application) on a YubiKey which may or may not be supported, and which can be enabled or disabled.
11+
public enum Capability: UInt {
12+
/// Identifies the YubiOTP application.
13+
case OTP = 0x0001
14+
/// Identifies the U2F (CTAP1) portion of the FIDO application.
15+
case U2F = 0x0002
16+
/// Identifies the OpenPGP application, implementing the OpenPGP Card protocol.
17+
case OPENPGP = 0x0008
18+
/// Identifies the PIV application, implementing the PIV protocol.
19+
case PIV = 0x0010
20+
/// Identifies the OATH application, implementing the YKOATH protocol.
21+
case OATH = 0x0020
22+
/// Identifies the HSMAUTH application.
23+
case HSMAUTH = 0x0100
24+
/// Identifies the FIDO2 (CTAP2) portion of the FIDO application.
25+
case FIDO2 = 0x0200
26+
27+
var bit: UInt { self.rawValue }
28+
}
29+
30+
31+
extension Capability {
32+
internal static func translateMaskFrom(fipsMask: UInt) -> UInt {
33+
var capabilities: UInt = 0;
34+
if fipsMask & 0b00000001 != 0 {
35+
capabilities |= Capability.FIDO2.bit;
36+
}
37+
if fipsMask & 0b00000010 != 0 {
38+
capabilities |= Capability.PIV.bit;
39+
}
40+
if fipsMask & 0b00000100 != 0 {
41+
capabilities |= Capability.OPENPGP.bit;
42+
}
43+
if fipsMask & 0b00001000 != 0 {
44+
capabilities |= Capability.OATH.bit;
45+
}
46+
if fipsMask & 0b00010000 != 0 {
47+
capabilities |= Capability.HSMAUTH.bit;
48+
}
49+
return capabilities;
50+
}
51+
}

YubiKit/YubiKit/Management/DeviceConfig.swift

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,66 @@ public struct DeviceConfig {
2222
public let challengeResponseTimeout: TimeInterval?
2323
public let deviceFlags: UInt8?
2424
public let enabledCapabilities: [DeviceTransport: UInt]
25+
public let isNFCRestricted: Bool?
2526

2627
internal let tagUSBEnabled: TKTLVTag = 0x03
2728
internal let tagAutoEjectTimeout: TKTLVTag = 0x06
2829
internal let tagChallengeResponseTimeout: TKTLVTag = 0x07
2930
internal let tagDeviceFlags: TKTLVTag = 0x08
31+
internal let tagNFCSupported: TKTLVTag = 0x0d
3032
internal let tagNFCEnabled: TKTLVTag = 0x0e
3133
internal let tagConfigurationLock: TKTLVTag = 0x0a
3234
internal let tagUnlock: TKTLVTag = 0x0b
3335
internal let tagReboot: TKTLVTag = 0x0c
36+
internal let tagNFCRestricted: TKTLVTag = 0x17
3437

35-
public func isApplicationEnabled(_ application: ApplicationType, overTransport transport: DeviceTransport) -> Bool {
38+
internal init(withTlvs tlvs: [TKTLVTag : Data], version: Version) throws {
39+
if let timeout = tlvs[tagAutoEjectTimeout]?.integer {
40+
self.autoEjectTimeout = TimeInterval(timeout)
41+
} else {
42+
self.autoEjectTimeout = 0
43+
}
44+
45+
if let timeout = tlvs[tagChallengeResponseTimeout]?.integer {
46+
self.challengeResponseTimeout = TimeInterval(timeout)
47+
} else {
48+
self.challengeResponseTimeout = 0
49+
}
50+
51+
self.deviceFlags = tlvs[tagDeviceFlags]?.uint8
52+
53+
var enabledCapabilities = [DeviceTransport: UInt]()
54+
if tlvs[tagUSBEnabled] != nil && version.major != 4 {
55+
// YK4 reports this incorrectly, instead use supportedCapabilities and USB mode.
56+
enabledCapabilities[DeviceTransport.usb] = tlvs[tagUSBEnabled]?.integer ?? 0
57+
}
58+
59+
if tlvs[tagNFCSupported] != nil {
60+
enabledCapabilities[DeviceTransport.nfc] = tlvs[tagNFCEnabled]?.integer ?? 0
61+
}
62+
self.enabledCapabilities = enabledCapabilities
63+
if let isNFCRestricted = tlvs[tagNFCRestricted]?.integer {
64+
self.isNFCRestricted = isNFCRestricted == 1
65+
} else {
66+
self.isNFCRestricted = nil
67+
}
68+
}
69+
70+
public func isApplicationEnabled(_ application: Capability, overTransport transport: DeviceTransport) -> Bool {
3671
guard let mask = enabledCapabilities[transport] else { return false }
3772
return (mask & application.rawValue) == application.rawValue
3873
}
3974

40-
public func deviceConfig(enabling: Bool, application: ApplicationType, overTransport transport: DeviceTransport) -> DeviceConfig? {
75+
76+
private init(autoEjectTimeout: TimeInterval?, challengeResponseTimeout: TimeInterval?, deviceFlags: UInt8?, enabledCapabilities: [DeviceTransport : UInt], isNFCRestricted: Bool?) {
77+
self.autoEjectTimeout = autoEjectTimeout
78+
self.challengeResponseTimeout = challengeResponseTimeout
79+
self.deviceFlags = deviceFlags
80+
self.enabledCapabilities = enabledCapabilities
81+
self.isNFCRestricted = isNFCRestricted
82+
}
83+
84+
public func deviceConfig(enabling: Bool, application: Capability, overTransport transport: DeviceTransport) -> DeviceConfig? {
4185
guard let oldMask = enabledCapabilities[transport] else { return nil }
4286
let newMask = enabling ? oldMask | application.rawValue : oldMask & ~application.rawValue
4387
var newEnabledCapabilities = enabledCapabilities
@@ -46,11 +90,16 @@ public struct DeviceConfig {
4690
return DeviceConfig(autoEjectTimeout: autoEjectTimeout,
4791
challengeResponseTimeout: challengeResponseTimeout,
4892
deviceFlags: deviceFlags,
49-
enabledCapabilities: newEnabledCapabilities)
93+
enabledCapabilities: newEnabledCapabilities,
94+
isNFCRestricted: self.isNFCRestricted)
5095
}
5196

5297
public func deviceConfig(autoEjectTimeout: TimeInterval, challengeResponseTimeout: TimeInterval) -> DeviceConfig {
53-
return Self.init(autoEjectTimeout: autoEjectTimeout, challengeResponseTimeout: challengeResponseTimeout, deviceFlags: self.deviceFlags, enabledCapabilities: self.enabledCapabilities)
98+
return Self.init(autoEjectTimeout: autoEjectTimeout, challengeResponseTimeout: challengeResponseTimeout, deviceFlags: self.deviceFlags, enabledCapabilities: self.enabledCapabilities, isNFCRestricted: self.isNFCRestricted)
99+
}
100+
101+
public func deviceConfig(nfcRestricted: Bool) -> DeviceConfig {
102+
return Self.init(autoEjectTimeout: self.autoEjectTimeout, challengeResponseTimeout: self.challengeResponseTimeout, deviceFlags: self.deviceFlags, enabledCapabilities: self.enabledCapabilities, isNFCRestricted: nfcRestricted)
54103
}
55104

56105
internal func data(reboot: Bool, lockCode: Data?, newLockCode: Data?) throws -> Data {
@@ -80,6 +129,11 @@ public struct DeviceConfig {
80129
if let newLockCode {
81130
data.append(TKBERTLVRecord(tag: tagConfigurationLock, value: newLockCode).data)
82131
}
132+
133+
if let isNFCRestricted, isNFCRestricted {
134+
data.append(TKBERTLVRecord(tag: tagNFCRestricted, value: UInt8(0x01).data).data)
135+
}
136+
83137
guard data.count <= 0xff else { throw ManagementSessionError.configTooLarge }
84138

85139
return UInt8(data.count).data + data

0 commit comments

Comments
 (0)