Skip to content

RSA Support #88

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 14 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 4 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/unrelentingtech/SwiftCBOR.git", from: "0.4.7"),
.package(url: "https://github.com/apple/swift-crypto.git", "2.0.0" ..< "4.0.0"),
.package(url: "https://github.com/apple/swift-crypto.git", "3.8.1" ..< "4.0.0"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0xTim Do you happen to know if there is an earlier version that supports RSA? My local package.resolved file initially didn't work, but I already lost the version it was locked to by the time I realized I could search from there. I think https://github.com/apple/swift-crypto/releases/tag/3.8.1 is the earliest we can go, but perhaps you know of an earlier version.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there is

(And to be honest I'd be happy to just start with the latest release anyway)

.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin.git", from: "1.1.0")
],
Expand All @@ -41,7 +41,9 @@ let package = Package(
.testTarget(
name: "WebAuthnTests",
dependencies: [
.target(name: "WebAuthn")
.target(name: "WebAuthn"),
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "_CryptoExtras", package: "swift-crypto"),
]
)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,39 @@ import Crypto
/// [https://www.w3.org/TR/webauthn/#biblio-iana-cose-algs-reg], for instance, -7 for "ES256" and -257 for "RS256".
public enum COSEAlgorithmIdentifier: Int, RawRepresentable, CaseIterable, Encodable, Sendable {
/// AlgES256 ECDSA with SHA-256
case algES256 = -7
/// AlgES384 ECDSA with SHA-384
case algES384 = -35
/// AlgES512 ECDSA with SHA-512
case algES512 = -36

// We don't support RSA yet

// /// AlgRS1 RSASSA-PKCS1-v1_5 with SHA-1
// case algRS1 = -65535
// /// AlgRS256 RSASSA-PKCS1-v1_5 with SHA-256
// case algRS256 = -257
// /// AlgRS384 RSASSA-PKCS1-v1_5 with SHA-384
// case algRS384 = -258
// /// AlgRS512 RSASSA-PKCS1-v1_5 with SHA-512
// case algRS512 = -259
// /// AlgPS256 RSASSA-PSS with SHA-256
// case algPS256 = -37
// /// AlgPS384 RSASSA-PSS with SHA-384
// case algPS384 = -38
// /// AlgPS512 RSASSA-PSS with SHA-512
// case algPS512 = -39
// // AlgEdDSA EdDSA
// case algEdDSA = -8

func hashAndCompare(data: Data, to compareHash: Data) -> Bool {
switch self {
case .algES256:
return SHA256.hash(data: data) == compareHash
case .algES384:
return SHA384.hash(data: data) == compareHash
case .algES512:
return SHA512.hash(data: data) == compareHash
}
}
case algES256 = -7
/// AlgES384 ECDSA with SHA-384
case algES384 = -35
/// AlgES512 ECDSA with SHA-512
case algES512 = -36
/// AlgRS1 RSASSA-PKCS1-v1_5 with SHA-1
case algRS1 = -65535
/// AlgRS256 RSASSA-PKCS1-v1_5 with SHA-256
case algRS256 = -257
/// AlgRS384 RSASSA-PKCS1-v1_5 with SHA-384
case algRS384 = -258
/// AlgRS512 RSASSA-PKCS1-v1_5 with SHA-512
case algRS512 = -259
/// AlgPS256 RSASSA-PSS with SHA-256
case algPS256 = -37
/// AlgPS384 RSASSA-PSS with SHA-384
case algPS384 = -38
/// AlgPS512 RSASSA-PSS with SHA-512
case algPS512 = -39
// /// AlgEdDSA EdDSA
// case algEdDSA = -8
func hashAndCompare(data: Data, to compareHash: Data) -> Bool {
switch self {
case .algES256, .algRS256, .algPS256:
return SHA256.hash(data: data) == compareHash
case .algES384, .algRS384, .algPS384:
return SHA384.hash(data: data) == compareHash
case .algES512, .algRS512, .algPS512:
return SHA512.hash(data: data) == compareHash
case .algRS1:
return Insecure.SHA1.hash(data: data) == compareHash
}
}
}
41 changes: 18 additions & 23 deletions Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ enum CredentialPublicKey: Sendable {
case .ellipticKey:
self = try .ec2(EC2PublicKey(publicKeyObject: publicKeyObject, algorithm: algorithm))
case .rsaKey:
throw WebAuthnError.unsupported
// self = try .rsa(RSAPublicKeyData(publicKeyObject: publicKeyObject, algorithm: algorithm))
self = try .rsa(RSAPublicKeyData(publicKeyObject: publicKeyObject, algorithm: algorithm))
case .octetKey:
throw WebAuthnError.unsupported
// self = try .okp(OKPPublicKey(publicKeyObject: publicKeyObject, algorithm: algorithm))
Expand Down Expand Up @@ -153,11 +152,12 @@ struct EC2PublicKey: PublicKey, Sendable {
.isValidSignature(ecdsaSignature, for: data) else {
throw WebAuthnError.invalidSignature
}
default:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we may want to list out the unsupported types here to catch new key sizes in the future, but the list may be a long one, so just a nit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm less worried about this. The want to throw an unsupported and any new supported ones will need to be added to the switch to make tests pass

throw WebAuthnError.unsupportedCredentialPublicKeyAlgorithm
}
}
}

/// Currently not in use
struct RSAPublicKeyData: PublicKey, Sendable {
let algorithm: COSEAlgorithmIdentifier
// swiftlint:disable:next identifier_name
Expand All @@ -184,26 +184,21 @@ struct RSAPublicKeyData: PublicKey, Sendable {
}

func verify(signature: some DataProtocol, data: some DataProtocol) throws {
throw WebAuthnError.unsupported
// let rsaSignature = _RSA.Signing.RSASignature(derRepresentation: signature)

// var rsaPadding: _RSA.Signing.Padding
// switch algorithm {
// case .algRS1, .algRS256, .algRS384, .algRS512:
// rsaPadding = .insecurePKCS1v1_5
// case .algPS256, .algPS384, .algPS512:
// rsaPadding = .PSS
// default:
// throw WebAuthnError.unsupportedCOSEAlgorithmForRSAPublicKey
// }

// guard try _RSA.Signing.PublicKey(rawRepresentation: rawRepresentation).isValidSignature(
// rsaSignature,
// for: data,
// padding: rsaPadding
// ) else {
// throw WebAuthnError.invalidSignature
// }
let rsaSignature = _RSA.Signing.RSASignature(rawRepresentation: signature)

var rsaPadding: _RSA.Signing.Padding
switch algorithm {
case .algRS1, .algRS256, .algRS384, .algRS512:
rsaPadding = .insecurePKCS1v1_5
case .algPS256, .algPS384, .algPS512:
rsaPadding = .PSS
default:
throw WebAuthnError.unsupportedCOSEAlgorithmForRSAPublicKey
}

let publicKey = try _RSA.Signing.PublicKey(n: n, e: e)
guard publicKey.isValidSignature(rsaSignature, for: data, padding: rsaPadding)
else { throw WebAuthnError.invalidSignature }
}
}

Expand Down
53 changes: 44 additions & 9 deletions Tests/WebAuthnTests/Utils/TestModels/TestAttestationObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,21 @@
//===----------------------------------------------------------------------===//

import WebAuthn
import SwiftCBOR
@preconcurrency import SwiftCBOR
import Testing

// protocol AttestationObjectParameter: CBOR {}

struct TestAttestationObject {
var fmt: CBOR?
var attStmt: CBOR?
var authData: CBOR?
var authData: AuthData = .none

enum AuthData {
case structured(TestAuthData)
case cbor(CBOR)
case none
}

var cborEncoded: [UInt8] {
var attestationObject: [CBOR: CBOR] = [:]
Expand All @@ -29,8 +36,12 @@ struct TestAttestationObject {
if let attStmt {
attestationObject[.utf8String("attStmt")] = attStmt
}
if let authData {
switch authData {
case .structured(let authData):
attestationObject[.utf8String("authData")] = .byteString(authData.byteArrayRepresentation)
case .cbor(let authData):
attestationObject[.utf8String("authData")] = authData
case .none: break
}

return [UInt8](CBOR.map(attestationObject).encode())
Expand All @@ -44,11 +55,22 @@ struct TestAttestationObjectBuilder {
self.wrapped = wrapped
}

func validMock() -> Self {
func keyAgnosticBase() -> Self {
var temp = self
temp.wrapped.fmt = .utf8String("none")
temp.wrapped.attStmt = .map([:])
temp.wrapped.authData = .byteString(TestAuthDataBuilder().validMock().build().byteArrayRepresentation)
return temp
}

func validMockECDSA() -> Self {
var temp = self.keyAgnosticBase()
temp.wrapped.authData = .structured(TestAuthDataBuilder().validMockECDSA().build())
return temp
}

func validMockRSA() -> Self {
var temp = self.keyAgnosticBase()
temp.wrapped.authData = .structured(TestAuthDataBuilder().validMockRSA().build())
return temp
}

Expand Down Expand Up @@ -104,25 +126,38 @@ struct TestAttestationObjectBuilder {

func invalidAuthData() -> Self {
var temp = self
temp.wrapped.authData = .double(1)
temp.wrapped.authData = .cbor(.double(1))
return temp
}

func emptyAuthData() -> Self {
var temp = self
temp.wrapped.authData = .byteString([])
temp.wrapped.authData = .cbor(.byteString([]))
return temp
}

func zeroAuthData(byteCount: Int) -> Self {
var temp = self
temp.wrapped.authData = .byteString([UInt8](repeating: 0, count: byteCount))
temp.wrapped.authData = .cbor(.byteString([UInt8](repeating: 0, count: byteCount)))
return temp
}

func authData(_ builder: TestAuthDataBuilder) -> Self {
var temp = self
temp.wrapped.authData = .byteString(builder.build().byteArrayRepresentation)
temp.wrapped.authData = .structured(builder.build())
return temp
}

func authData(builder: (TestAuthDataBuilder) -> TestAuthDataBuilder) -> Self {
var temp = self
switch temp.wrapped.authData {
case .structured(let testAuthData):
temp.wrapped.authData = .structured(builder(.init(wrapped: testAuthData)).build())
case .cbor:
Issue.record("authData must be structured")
case .none:
temp.wrapped.authData = .structured(builder(.init()).build())
}
return temp
}

Expand Down
17 changes: 15 additions & 2 deletions Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,28 @@ struct TestAuthDataBuilder {
build().byteArrayRepresentation.base64URLEncodedString()
}

func validMock() -> Self {
func validMockECDSA() -> Self {
self
.relyingPartyIDHash(fromRelyingPartyID: "example.com")
.flags(0b11000101)
.counter([0b00000000, 0b00000000, 0b00000000, 0b00000000])
.attestedCredData(
credentialIDLength: [0b00000000, 0b00000001],
credentialID: [0b00000001],
credentialPublicKey: TestCredentialPublicKeyBuilder().validMock().buildAsByteArray()
credentialPublicKey: TestCredentialPublicKeyBuilder().validMockECDSA().buildAsByteArray()
)
.extensions([UInt8](repeating: 0, count: 20))
}

func validMockRSA() -> Self {
self
.relyingPartyIDHash(fromRelyingPartyID: "example.com")
.flags(0b11000101)
.counter([0b00000000, 0b00000000, 0b00000000, 0b00000000])
.attestedCredData(
credentialIDLength: [0b00000000, 0b00000001],
credentialID: [0b00000001],
credentialPublicKey: TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray()
)
.extensions([UInt8](repeating: 0, count: 20))
}
Expand Down
41 changes: 39 additions & 2 deletions Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,22 @@
//===----------------------------------------------------------------------===//

@testable import WebAuthn
import SwiftCBOR
@preconcurrency import SwiftCBOR

struct TestCredentialPublicKey {
var kty: CBOR?
var alg: CBOR?
// EC2, OKP
var crv: CBOR?
var xCoordinate: CBOR?

//EC2
var yCoordinate: CBOR?

// RSA
var nCoordinate: CBOR?
var eCoordinate: CBOR?

var byteArrayRepresentation: [UInt8] {
var value: [CBOR: CBOR] = [:]
if let kty {
Expand All @@ -38,6 +45,15 @@ struct TestCredentialPublicKey {
if let yCoordinate {
value[COSEKey.y.cbor] = yCoordinate
}

if let nCoordinate {
value[COSEKey.n.cbor] = nCoordinate
}

if let eCoordinate {
value[COSEKey.e.cbor] = eCoordinate
}

return CBOR.map(value).encode()
}
}
Expand All @@ -53,14 +69,23 @@ struct TestCredentialPublicKeyBuilder {
return wrapped.byteArrayRepresentation
}

func validMock() -> Self {
func validMockECDSA() -> Self {
return self
.kty(.ellipticKey)
.crv(.p256)
.alg(.algES256)
.xCoordinate(TestECCKeyPair.publicKeyXCoordinate)
.yCoordiante(TestECCKeyPair.publicKeyYCoordinate)
}

func validMockRSA() -> Self {
return self
.kty(.rsaKey)
.alg(.algRS256)
.nCoordinate(TestRSAKeyPair.publicKeyNCoordinate)
.eCoordiante(TestRSAKeyPair.publicKeyECoordinate)
}


func kty(_ kty: COSEKeyType) -> Self {
var temp = self
Expand Down Expand Up @@ -91,4 +116,16 @@ struct TestCredentialPublicKeyBuilder {
temp.wrapped.yCoordinate = .byteString(yCoordinate)
return temp
}

func nCoordinate(_ nCoordinate: [UInt8]) -> Self {
var temp = self
temp.wrapped.nCoordinate = .byteString(nCoordinate)
return temp
}

func eCoordiante(_ eCoordinate: [UInt8]) -> Self {
var temp = self
temp.wrapped.eCoordinate = .byteString(eCoordinate)
return temp
}
}
Loading
Loading