Skip to content

PIV: Curve 25519 keys and api surface grooming #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Aug 11, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,321 changes: 757 additions & 564 deletions FullStackTests/Tests/PIVFullStackTests.swift

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions FullStackTests/Tests/SCP/SCP11FullStackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,10 @@ final class SCP11bFullStackTests: XCTestCase {

let chain = try await securityDomainSession.getCertificateBundle(scpKeyRef: scpKeyRef)
let first: X509Cert = chain.first!
let publicKey = first.publicKey!.asEC()! // make sure we can read the public key
guard case let .ec(publicKey) = first.publicKey! else {
XCTFail("Expected EC public key")
return
}

let params = try SCP11KeyParams(keyRef: scpKeyRef, pkSdEcka: publicKey)

Expand Down Expand Up @@ -297,7 +300,10 @@ extension SecurityDomainSession {

// Upload the CA public key to the YubiKey so it can verify signatures
let ca = Scp11TestData.caCert
let certificatePublicKey = ca.publicKey!.asEC()!
guard case let .ec(certificatePublicKey) = ca.publicKey! else {
XCTFail("Expected EC public key")
return nil
}
try await putKey(keyRef: oceRef, publicKey: certificatePublicKey, replaceKvn: 0)

// Extract the CA certificate's Subject Key Identifier for issuer referencing
Expand Down
2 changes: 1 addition & 1 deletion FullStackTests/Tests/SCP/SCPFullStackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class SCPFullStackTests: XCTestCase {
let scpKeyRef = SCPKeyRef(kid: .scp11b, kvn: 0x01)
let certificates = try await securityDomainSession.getCertificateBundle(scpKeyRef: scpKeyRef)
guard let last = certificates.last,
let publicKey = last.publicKey?.asEC()
case let .ec(publicKey) = last.publicKey
else {
XCTFail()
return
Expand Down
159 changes: 159 additions & 0 deletions YubiKit/UnitTests/Curve25519KeysTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright Yubico AB
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import CryptoKit
import Foundation
import Testing

@testable import YubiKit

/// Tests for Ed25519 and X25519 key handling and validation.
struct Curve25519KeysTests {

// MARK: - Ed25519 Tests

/// Test Ed25519 public key creation with valid data.
@Test func ed25519PublicKeyValidData() {
// Generate a valid Ed25519 key pair using CryptoKit
let cryptoKitPrivateKey = Curve25519.Signing.PrivateKey()
let validKeyData = cryptoKitPrivateKey.publicKey.rawRepresentation
let publicKey = Ed25519.PublicKey(keyData: validKeyData)

#expect(publicKey != nil)
#expect(publicKey?.keyData == validKeyData)
#expect(publicKey?.keyData.count == 32)
}

/// Test Ed25519 public key creation with invalid data.
@Test func ed25519PublicKeyInvalidData() {
let invalidKeyData31 = Data(repeating: 0x01, count: 31)
let invalidKeyData33 = Data(repeating: 0x01, count: 33)
let emptyData = Data()

#expect(Ed25519.PublicKey(keyData: invalidKeyData31) == nil)
#expect(Ed25519.PublicKey(keyData: invalidKeyData33) == nil)
#expect(Ed25519.PublicKey(keyData: emptyData) == nil)
}

/// Test Ed25519 private key creation with valid data.
@Test func ed25519PrivateKeyValidData() {
// Generate a valid Ed25519 key pair using CryptoKit
let cryptoKitPrivateKey = Curve25519.Signing.PrivateKey()
let validSeed = cryptoKitPrivateKey.rawRepresentation
let validPublicKeyData = cryptoKitPrivateKey.publicKey.rawRepresentation
let publicKey = Ed25519.PublicKey(keyData: validPublicKeyData)!

let privateKey = Ed25519.PrivateKey(seed: validSeed, publicKey: publicKey)

#expect(privateKey != nil)
#expect(privateKey?.seed == validSeed)
#expect(privateKey?.publicKey == publicKey)
#expect(privateKey?.seed.count == 32)
}

/// Test Ed25519 private key creation with invalid data.
@Test func ed25519PrivateKeyInvalidData() {
let invalidSeed31 = Data(repeating: 0x02, count: 31)
let invalidSeed33 = Data(repeating: 0x02, count: 33)
// Generate a valid public key for testing
let cryptoKitPrivateKey = Curve25519.Signing.PrivateKey()
let validPublicKeyData = cryptoKitPrivateKey.publicKey.rawRepresentation
let publicKey = Ed25519.PublicKey(keyData: validPublicKeyData)!

#expect(Ed25519.PrivateKey(seed: invalidSeed31, publicKey: publicKey) == nil)
#expect(Ed25519.PrivateKey(seed: invalidSeed33, publicKey: publicKey) == nil)
}

// MARK: - X25519 Tests

/// Test X25519 public key creation with valid data.
@Test func x25519PublicKeyValidData() {
// Generate a valid X25519 key pair using CryptoKit
let cryptoKitPrivateKey = Curve25519.KeyAgreement.PrivateKey()
let validKeyData = cryptoKitPrivateKey.publicKey.rawRepresentation
let publicKey = X25519.PublicKey(keyData: validKeyData)

#expect(publicKey != nil)
#expect(publicKey?.keyData == validKeyData)
#expect(publicKey?.keyData.count == 32)
}

/// Test X25519 public key creation with invalid data.
@Test func x25519PublicKeyInvalidData() {
let invalidKeyData31 = Data(repeating: 0x04, count: 31)
let invalidKeyData33 = Data(repeating: 0x04, count: 33)
let emptyData = Data()

#expect(X25519.PublicKey(keyData: invalidKeyData31) == nil)
#expect(X25519.PublicKey(keyData: invalidKeyData33) == nil)
#expect(X25519.PublicKey(keyData: emptyData) == nil)
}

/// Test X25519 private key creation with valid data.
@Test func x25519PrivateKeyValidData() {
// Generate a valid X25519 key pair using CryptoKit
let cryptoKitPrivateKey = Curve25519.KeyAgreement.PrivateKey()
let validScalar = cryptoKitPrivateKey.rawRepresentation
let validPublicKeyData = cryptoKitPrivateKey.publicKey.rawRepresentation
let publicKey = X25519.PublicKey(keyData: validPublicKeyData)!

let privateKey = X25519.PrivateKey(scalar: validScalar, publicKey: publicKey)

#expect(privateKey != nil)
#expect(privateKey?.scalar == validScalar)
#expect(privateKey?.publicKey == publicKey)
#expect(privateKey?.scalar.count == 32)
}

/// Test X25519 private key creation with invalid data.
@Test func x25519PrivateKeyInvalidData() {
let invalidScalar31 = Data(repeating: 0x05, count: 31)
let invalidScalar33 = Data(repeating: 0x05, count: 33)
// Generate a valid public key for testing
let cryptoKitPrivateKey = Curve25519.KeyAgreement.PrivateKey()
let validPublicKeyData = cryptoKitPrivateKey.publicKey.rawRepresentation
let publicKey = X25519.PublicKey(keyData: validPublicKeyData)!

#expect(X25519.PrivateKey(scalar: invalidScalar31, publicKey: publicKey) == nil)
#expect(X25519.PrivateKey(scalar: invalidScalar33, publicKey: publicKey) == nil)
}

// MARK: - Equality Tests

/// Test Ed25519 key equality.
@Test func ed25519KeyEquality() {
let keyData1 = Data(repeating: 0x07, count: 32)
let keyData2 = Data(repeating: 0x08, count: 32)

let publicKey1a = Ed25519.PublicKey(keyData: keyData1)!
let publicKey1b = Ed25519.PublicKey(keyData: keyData1)!
let publicKey2 = Ed25519.PublicKey(keyData: keyData2)!

#expect(publicKey1a == publicKey1b)
#expect(publicKey1a != publicKey2)
}

/// Test X25519 key equality.
@Test func x25519KeyEquality() {
let keyData1 = Data(repeating: 0x09, count: 32)
let keyData2 = Data(repeating: 0x0A, count: 32)

let publicKey1a = X25519.PublicKey(keyData: keyData1)!
let publicKey1b = X25519.PublicKey(keyData: keyData1)!
let publicKey2 = X25519.PublicKey(keyData: keyData2)!

#expect(publicKey1a == publicKey1b)
#expect(publicKey1a != publicKey2)
}
}
177 changes: 177 additions & 0 deletions YubiKit/UnitTests/PIVDataFormatterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright Yubico AB
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import CommonCrypto
import CryptoKit
import Foundation
import Testing

@testable import YubiKit

struct PIVDataFormatterTests {

// Test data for ECDSA signing with messages
struct ECDSATestCase: Sendable {
let curve: EC.Curve
let algorithm: PIV.ECDSASignatureAlgorithm
let expectedHex: String
}

@Test(
"Prepare ECDSA Signing",
arguments: [
ECDSATestCase(
curve: .p256,
algorithm: .message(.sha256),
expectedHex: "c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a"
),
ECDSATestCase(
curve: .p384,
algorithm: .message(.sha256),
expectedHex:
"00000000000000000000000000000000c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a"
),
ECDSATestCase(
curve: .p256,
algorithm: .message(.sha1),
expectedHex: "000000000000000000000000d3486ae9136e7856bc42212385ea797094475802"
),
ECDSATestCase(
curve: .p256,
algorithm: .message(.sha512),
expectedHex: "f6cde2a0f819314cdde55fc227d8d7dae3d28cc556222a0a8ad66d91ccad4aad"
),
ECDSATestCase(
curve: .p384,
algorithm: .message(.sha512),
expectedHex:
"f6cde2a0f819314cdde55fc227d8d7dae3d28cc556222a0a8ad66d91ccad4aad6094f517a2182360c9aacf6a3dc32316"
),
]
)
func prepareECDSASigning(testCase: ECDSATestCase) throws {
let data = "Hello world!".data(using: .utf8)!
let result = PIVDataFormatter.prepareDataForECDSASigning(
data,
curve: testCase.curve,
algorithm: testCase.algorithm
)
let expected = Data(hexEncodedString: testCase.expectedHex)!
#expect(result == expected, "Got \(result.hexEncodedString), expected: \(expected.hexEncodedString)")
}

@Test func prepareECDSADigestSigning() throws {
let data = Data(hexEncodedString: "c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a")!
do {
let result = PIVDataFormatter.prepareDataForECDSASigning(
data,
curve: .p256,
algorithm: .digest(.sha256)
)
let expected = Data(hexEncodedString: "c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a")!
#expect(result == expected, "Got \(result.hexEncodedString), expected: \(expected.hexEncodedString)")
}
}

// Test data for RSA signing
struct RSATestCase {
let algorithm: PIV.RSASignatureAlgorithm
let expectedHex: String
}

@Test(
"Prepare RSA Signing",
arguments: [
RSATestCase(
algorithm: .pkcs1v15(.sha256),
expectedHex:
"0001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d060960864801650304020105000420c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a"
),
RSATestCase(
algorithm: .pkcs1v15(.sha1),
expectedHex:
"0001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003021300906052b0e03021a05000414d3486ae9136e7856bc42212385ea797094475802"
),
]
)
func prepareRSASigning(testCase: RSATestCase) throws {
let data = "Hello world!".data(using: .utf8)!
let result = try PIVDataFormatter.prepareDataForRSASigning(
data,
keySize: .bits1024,
algorithm: testCase.algorithm
)
let expected = Data(hexEncodedString: testCase.expectedHex)!
#expect(result == expected, "Got \(result.hexEncodedString), expected: \(expected.hexEncodedString)")
}

@Test func prepareECDSADigestSigningP384WithPadding() throws {
let data = "Hello world!".data(using: .utf8)!
do {
let result = PIVDataFormatter.prepareDataForECDSASigning(
data,
curve: .p384,
algorithm: .digest(.sha256)
)
let expected = Data(
hexEncodedString:
"00000000000000000000000000000000000000000000000000000000000000000000000048656c6c6f20776f726c6421"
)!
#expect(result == expected, "Got \(result.hexEncodedString), expected: \(expected.hexEncodedString)")
}
}

// Test data for RSA decryption
struct RSADecryptionTestCase {
let algorithm: PIV.RSAEncryptionAlgorithm
let encryptedHex: String
}

@Test(
"Extract RSA Encryption",
arguments: [
RSADecryptionTestCase(
algorithm: .pkcs1v15,
encryptedHex:
"00022b781255b78f9570844701748107f506effbea5f0822b41dded192938906cefe16eef190d4cf7f7b0866badf94ca0e4e08fda43e4619edec2703987a56a78aa4c2d36a8f89c43f1f9c0ab681e45a759744ef946d65d95e74536b28b83cdc1c62e36c014c8b4a50c178a54306ce7395240e0048656c6c6f20576f726c6421"
),
RSADecryptionTestCase(
algorithm: .oaep(.sha224),
encryptedHex:
"00bcbb35b6ef5c94a85fb3439a6dabda617a08963cf81023bac19c619b024cb71b8aee25cc30991279c908198ba623fba88547741dbf17a6f2a737ec95542b56b2b429bea8bd3145af7c8f144dcf804b89d3f9de21d6d6dc852fc91c666b8582bf348e1388ac2f54651ae6a1f5355c8d96daf96c922a9f1a499d890412d09454"
),
]
)
func extractRSAEncryption(testCase: RSADecryptionTestCase) throws {
let data = Data(hexEncodedString: testCase.encryptedHex)!
let result = try PIVDataFormatter.extractDataFromRSAEncryption(data, algorithm: testCase.algorithm)
let expected = "Hello World!".data(using: .utf8)!
#expect(result == expected, "Got \(result.hexEncodedString), expected: \(expected.hexEncodedString)")
}

@Test func extractMalformedRSAData() throws {
let data = Data(
hexEncodedString:
"79ce573cfc2bdfe835175ffd4bd01ab35eccfd31e2b009a1943123e9cb2db4878608c821fb96a6c63382aaf1c12ce0f03b83"
)!
do {
let _ = try PIVDataFormatter.extractDataFromRSAEncryption(data, algorithm: .pkcs1v15)
Issue.record("extractDataFromRSAEncryption returned although the data had the wrong size.")
} catch PIV.SessionError.invalidDataSize {
#expect(true, "Failed as expected with invalid data size error")
} catch {
Issue.record("Unexpected error: \(error)")
}
}
}
Loading
Loading