Skip to content

Commit

Permalink
Merge branch 'jens/piv-move-delete-key'
Browse files Browse the repository at this point in the history
  • Loading branch information
jensutbult committed May 28, 2024
2 parents a7176f3 + 965bea0 commit 80771d6
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 36 deletions.
51 changes: 45 additions & 6 deletions FullStackTests/Tests/PIVFullStackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,45 @@ final class PIVFullStackTests: XCTestCase {
}
}

func testMoveKey() throws {
runAuthenticatedPIVTest { session in
guard session.supports(PIVSessionFeature.moveDelete) else { print("⚠️ Skip testMoveKey()"); return }
try await session.putCertificate(certificate: self.testCertificate, inSlot: .authentication)
try await session.putCertificate(certificate: self.testCertificate, inSlot: .signature)
let publicKey = try await session.generateKeyInSlot(slot: .authentication, type: .RSA1024, pinPolicy: .always, touchPolicy: .always)
let authSlotMetadata = try await session.getSlotMetadata(.authentication)
XCTAssertEqual(publicKey, authSlotMetadata.publicKey)
try await session.moveKey(sourceSlot: .authentication, destinationSlot: .signature)
let signSlotMetadata = try await session.getSlotMetadata(.signature)
XCTAssertEqual(publicKey, signSlotMetadata.publicKey)
do {
_ = try await session.getSlotMetadata(.authentication)
XCTFail("Got metadata when we should have thrown a referenceDataNotFound exception.")
} catch {
guard let responseError = error as? ResponseError else { XCTFail("Unexpected error: \(error)"); return }
XCTAssertTrue(responseError.responseStatus.status == .referencedDataNotFound)
}
}
}

func testDeleteKey() throws {
runAuthenticatedPIVTest { session in
guard session.supports(PIVSessionFeature.moveDelete) else { print("⚠️ Skip testDeleteKey()"); return }
try await session.putCertificate(certificate: self.testCertificate, inSlot: .authentication, compress: true)
let publicKey = try await session.generateKeyInSlot(slot: .authentication, type: .RSA1024, pinPolicy: .always, touchPolicy: .always)
let slotMetadata = try await session.getSlotMetadata(.authentication)
XCTAssertEqual(publicKey, slotMetadata.publicKey)
try await session.deleteKey(in: .authentication)
do {
_ = try await session.getSlotMetadata(.authentication)
XCTFail("Got metadata when we should have thrown a referenceDataNotFound exception.")
} catch {
guard let responseError = error as? ResponseError else { XCTFail("Unexpected error: \(error)"); return }
XCTAssertTrue(responseError.responseStatus.status == .referencedDataNotFound)
}
}
}

func testPutCompressedAndReadCertificate() throws {
runAuthenticatedPIVTest { session in
try await session.putCertificate(certificate: self.testCertificate, inSlot: .authentication, compress: true)
Expand Down Expand Up @@ -462,29 +501,29 @@ final class PIVFullStackTests: XCTestCase {
func testSlotMetadata() throws {
runAuthenticatedPIVTest { session in
guard session.supports(PIVSessionFeature.metadata) else { print("⚠️ Skip testSlotMetadata()"); return }
_ = try await session.generateKeyInSlot(slot: .authentication, type: .ECCP256, pinPolicy: .always, touchPolicy: .always)
var publicKey = try await session.generateKeyInSlot(slot: .authentication, type: .ECCP256, pinPolicy: .always, touchPolicy: .always)
var metadata = try await session.getSlotMetadata(.authentication)
XCTAssertEqual(metadata.keyType, .ECCP256)
XCTAssertEqual(metadata.pinPolicy, .always)
XCTAssertEqual(metadata.touchPolicy, .always)
XCTAssertEqual(metadata.generated, true)
XCTAssertTrue(metadata.publicKey.count > 0)
XCTAssertEqual(metadata.publicKey, publicKey)

_ = try await session.generateKeyInSlot(slot: .authentication, type: .ECCP384, pinPolicy: .never, touchPolicy: .never)
publicKey = try await session.generateKeyInSlot(slot: .authentication, type: .ECCP384, pinPolicy: .never, touchPolicy: .never)
metadata = try await session.getSlotMetadata(.authentication)
XCTAssertEqual(metadata.keyType, .ECCP384)
XCTAssertEqual(metadata.pinPolicy, .never)
XCTAssertEqual(metadata.touchPolicy, .never)
XCTAssertEqual(metadata.generated, true)
XCTAssertTrue(metadata.publicKey.count > 0)
XCTAssertEqual(metadata.publicKey, publicKey)

_ = try await session.generateKeyInSlot(slot: .authentication, type: .ECCP256, pinPolicy: .once, touchPolicy: .cached)
publicKey = try await session.generateKeyInSlot(slot: .authentication, type: .ECCP256, pinPolicy: .once, touchPolicy: .cached)
metadata = try await session.getSlotMetadata(.authentication)
XCTAssertEqual(metadata.keyType, .ECCP256)
XCTAssertEqual(metadata.pinPolicy, .once)
XCTAssertEqual(metadata.touchPolicy, .cached)
XCTAssertEqual(metadata.generated, true)
XCTAssertTrue(metadata.publicKey.count > 0)
XCTAssertEqual(metadata.publicKey, publicKey)
}

}
Expand Down
2 changes: 1 addition & 1 deletion YubiKit/YubiKit/PIV/PIVDataTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ public struct PIVSlotMetadata {
/// Whether the key was generated on the YubiKey or imported.
public let generated: Bool
/// Returns the public key corresponding to the key in the slot.
public let publicKey: Data
public let publicKey: SecKey
}

/// Metadata about the PIN or PUK.
Expand Down
59 changes: 31 additions & 28 deletions YubiKit/YubiKit/PIV/PIVSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,34 +170,9 @@ public final actor PIVSession: Session, InternalSession {
let apdu = APDU(cla: 0, ins: insGenerateAsymetric, p1: 0, p2: slot.rawValue, command: tlvContainer.data)
let result = try await connection.send(apdu: apdu)
guard let records = TKBERTLVRecord.sequenceOfRecords(from: result),
let record = records.recordWithTag(0x7F49),
let records = TKBERTLVRecord.sequenceOfRecords(from: record.value)
let record = records.recordWithTag(0x7F49)
else { throw PIVSessionError.invalidResponse }
switch type {
case .ECCP256, .ECCP384:
guard let eccKeyData = records.recordWithTag(0x86)?.value else { throw PIVSessionError.invalidResponse }
let attributes = [kSecAttrKeyType: kSecAttrKeyTypeEC,
kSecAttrKeyClass: kSecAttrKeyClassPublic] as CFDictionary
var error: Unmanaged<CFError>?
guard let publicKey = SecKeyCreateWithData(eccKeyData as CFData, attributes, &error) else { throw error!.takeRetainedValue() as Error }
return publicKey
case .RSA1024, .RSA2048, .RSA3072, .RSA4096:
guard let modulus = records.recordWithTag(0x81)?.value,
let exponentData = records.recordWithTag(0x82)?.value
else { throw PIVSessionError.invalidResponse }
let modulusData = UInt8(0x00).data + modulus
var data = Data()
data.append(TKBERTLVRecord(tag: 0x02, value: modulusData).data)
data.append(TKBERTLVRecord(tag: 0x02, value: exponentData).data)
let keyRecord = TKBERTLVRecord(tag: 0x30, value: data)
let attributes = [kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeyClass: kSecAttrKeyClassPublic] as CFDictionary
var error: Unmanaged<CFError>?
guard let publicKey = SecKeyCreateWithData(keyRecord.data as CFData, attributes, &error) else { throw error!.takeRetainedValue() as Error }
return publicKey
case .unknown:
throw PIVSessionError.unknownKeyType
}
return try SecKey.secKey(fromYubiKeyData: record.value, type: type)
}

/// Import a private key into a slot.
Expand Down Expand Up @@ -260,6 +235,32 @@ public final actor PIVSession: Session, InternalSession {
return keyType
}

/// Move key from one slot to another. The source slot must not be the attestation slot and the
/// destination slot must be empty. This method requires authentication with the management key.
///
/// - Parameters:
/// - sourceSlot: Slot to move the key from.
/// - destinationSlot: Slot to move the key to.
public func moveKey(sourceSlot: PIVSlot, destinationSlot: PIVSlot) async throws {
guard self.supports(PIVSessionFeature.moveDelete) else { throw SessionError.notSupported }
guard sourceSlot != PIVSlot.attestation else { throw SessionError.illegalArgument }
guard let connection = _connection else { throw SessionError.noConnection }
Logger.piv.debug("Move key from \(String(describing: sourceSlot)) to \(String(describing: destinationSlot)), \(#function)")
let apdu = APDU(cla: 0, ins: insMoveKey, p1: destinationSlot.rawValue, p2: sourceSlot.rawValue)
try await connection.send(apdu: apdu)
}

/// Delete key from slot. This method requires authentication with the management key.
///
/// - Parameter slot: Slot to delete the key from.
public func deleteKey(in slot: PIVSlot) async throws {
guard self.supports(PIVSessionFeature.moveDelete) else { throw SessionError.notSupported }
guard let connection = _connection else { throw SessionError.noConnection }
Logger.piv.debug("Delete key in \(String(describing: slot)), \(#function)")
let apdu = APDU(cla: 0, ins: insMoveKey, p1: 0xff, p2: slot.rawValue)
try await connection.send(apdu: apdu)
}

/// Writes an X.509 certificate to a slot on the YubiKey.
///
/// This method requires authentication.
Expand Down Expand Up @@ -397,7 +398,8 @@ public final actor PIVSession: Session, InternalSession {
let pinPolicy = PIVPinPolicy(rawValue: policyBytes[indexPinPolicy]),
let touchPolicy = PIVTouchPolicy(rawValue: policyBytes[indexTouchPolicy]),
let origin = records.recordWithTag(tagMetadataOrigin)?.value.uint8,
let publicKey = records.recordWithTag(tagMetadataPublicKey)?.value
let publicKeyData = records.recordWithTag(tagMetadataPublicKey)?.value,
let publicKey = try? SecKey.secKey(fromYubiKeyData: publicKeyData, type: keyType)
else { throw PIVSessionError.dataParseError }

return PIVSlotMetadata(keyType: keyType, pinPolicy: pinPolicy, touchPolicy: touchPolicy, generated: origin == originGenerated, publicKey: publicKey)
Expand Down Expand Up @@ -735,6 +737,7 @@ fileprivate let insGetSerial: UInt8 = 0xf8
fileprivate let insGetMetadata: UInt8 = 0xf7
fileprivate let insGetData: UInt8 = 0xcb
fileprivate let insPutData: UInt8 = 0xdb
fileprivate let insMoveKey: UInt8 = 0xf6
fileprivate let insImportKey: UInt8 = 0xfe
fileprivate let insChangeReference: UInt8 = 0x24
fileprivate let insResetRetry: UInt8 = 0x2c
Expand Down
4 changes: 3 additions & 1 deletion YubiKit/YubiKit/PIV/PIVSessionFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import Foundation

public enum PIVSessionFeature: SessionFeature {

case usagePolicy, aesKey, serialNumber, metadata, attestation, p384, touchCached, rsaGeneration, rsa3072and4096
case usagePolicy, aesKey, serialNumber, metadata, attestation, p384, touchCached, rsaGeneration, rsa3072and4096, moveDelete

public func isSupported(by version: Version) -> Bool {
switch self {
Expand All @@ -38,6 +38,8 @@ public enum PIVSessionFeature: SessionFeature {
return version < Version(withString: "4.2.6")! || version >= Version(withString: "4.3.5")!
case .rsa3072and4096:
return version >= Version(withString: "5.7.0")!
case .moveDelete:
return version >= Version(withString: "5.7.0")!
}
}
}
30 changes: 30 additions & 0 deletions YubiKit/YubiKit/PIV/SecKey+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,39 @@
// limitations under the License.

import Foundation
import CryptoTokenKit

extension SecKey {

internal static func secKey(fromYubiKeyData data: Data, type: PIVKeyType) throws -> SecKey {
guard let records = TKBERTLVRecord.sequenceOfRecords(from: data) else { throw PIVSessionError.dataParseError }
switch type {
case .ECCP256, .ECCP384:
guard let eccKeyData = records.recordWithTag(0x86)?.value else { throw PIVSessionError.invalidResponse }
let attributes = [kSecAttrKeyType: kSecAttrKeyTypeEC,
kSecAttrKeyClass: kSecAttrKeyClassPublic] as CFDictionary
var error: Unmanaged<CFError>?
guard let publicKey = SecKeyCreateWithData(eccKeyData as CFData, attributes, &error) else { throw error!.takeRetainedValue() as Error }
return publicKey
case .RSA1024, .RSA2048, .RSA3072, .RSA4096:
guard let modulus = records.recordWithTag(0x81)?.value,
let exponentData = records.recordWithTag(0x82)?.value
else { throw PIVSessionError.invalidResponse }
let modulusData = UInt8(0x00).data + modulus
var data = Data()
data.append(TKBERTLVRecord(tag: 0x02, value: modulusData).data)
data.append(TKBERTLVRecord(tag: 0x02, value: exponentData).data)
let keyRecord = TKBERTLVRecord(tag: 0x30, value: data)
let attributes = [kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeyClass: kSecAttrKeyClassPublic] as CFDictionary
var error: Unmanaged<CFError>?
guard let publicKey = SecKeyCreateWithData(keyRecord.data as CFData, attributes, &error) else { throw error!.takeRetainedValue() as Error }
return publicKey
case .unknown:
throw PIVSessionError.unknownKeyType
}
}

var type: PIVKeyType? {
guard let attributes = SecKeyCopyAttributes(self) as? Dictionary<String, Any>,
let size = attributes[kSecAttrKeySizeInBits as String] as? UInt,
Expand Down
1 change: 1 addition & 0 deletions YubiKit/YubiKit/Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public struct ResponseStatus: Equatable {
case conditionsNotSatisfied = 0x6985
case commandNotAllowed = 0x6986
case incorrectParameters = 0x6A80
case referencedDataNotFound = 0x6a88
case fileNotFound = 0x6A82
case noSpace = 0x6A84
case wrongParametersP1P2 = 0x6B00
Expand Down
1 change: 1 addition & 0 deletions YubiKit/YubiKit/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ public enum SessionError: Error {
case activeSession
case missingApplication
case unexpectedStatusCode
case illegalArgument
}

0 comments on commit 80771d6

Please sign in to comment.