Skip to content
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

macos authenticator #337

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ webauthn-rs-demo-wasm/pkg
.DS_Store
local
build.sh
.swiftpm
.build
3 changes: 3 additions & 0 deletions webauthn-authenticator-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ softpasskey = ["crypto", "softtoken"]
softtoken = ["crypto", "ctap2"]
usb = ["ctap2", "dep:fido-hid-rs"]
win10 = ["dep:windows"]
macos = ["dep:swift-rs"]

default = []

Expand Down Expand Up @@ -71,6 +72,7 @@ authenticator = { version = "0.3.2-dev.1", optional = true, default-features = f

pcsc = { git = "https://github.com/bluetech/pcsc-rust.git", rev = "13e24649be96989cdffb7e73ca3a994b9534ddff", optional = true }
windows = { version = "0.41.0", optional = true, features = ["Win32_Graphics_Gdi", "Win32_Networking_WindowsWebServices", "Win32_Foundation", "Win32_UI_WindowsAndMessaging", "Win32_System_LibraryLoader", "Win32_Graphics_Dwm" ] }
swift-rs = { version = "1.0.5", optional = true }
serde.workspace = true
bitflags = "1.3.2"
unicode-normalization = "0.1.22"
Expand Down Expand Up @@ -107,6 +109,7 @@ image = ">= 0.23.14, < 0.24"

[build-dependencies]
openssl = { workspace = true, optional = true }
swift-rs = { version = "1.0.5", optional = true, features = ["build"] }

[[example]]
name = "authenticate"
Expand Down
10 changes: 10 additions & 0 deletions webauthn-authenticator-rs/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,17 @@ Please upgrade to OpenSSL v3.0.0 or later.
}
}

#[cfg(feature = "macos")]
fn macos() {
swift_rs::SwiftLinker::new("12")
.with_package("MacAuthn", "./src/MacAuthn")
.link();
}

fn main() {
#[cfg(feature = "crypto")]
crypto::test_openssl();

#[cfg(feature = "macos")]
macos();
}
6 changes: 6 additions & 0 deletions webauthn-authenticator-rs/examples/authenticate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ enum Provider {
#[cfg(feature = "win10")]
/// Windows 10 WebAuthn API, supporting BTLE, NFC and USB HID.
Win10,

#[cfg(feature = "macos")]
/// MacOS Authorization API
MacOS,
}

impl Provider {
Expand Down Expand Up @@ -209,6 +213,8 @@ impl Provider {
Provider::Mozilla => Box::<webauthn_authenticator_rs::mozilla::MozillaAuthenticator>::default(),
#[cfg(feature = "win10")]
Provider::Win10 => Box::<webauthn_authenticator_rs::win10::Win10>::default(),
#[cfg(feature = "macos")]
Provider::MacOS => Box::<webauthn_authenticator_rs::macos::MacOS>::default(),
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions webauthn-authenticator-rs/src/MacAuthn/Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "swift-rs",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Brendonovich/swift-rs",
"state" : {
"revision" : "a1578d5808b22b5a3461d36a6c8add5cd83a2ddf",
"version" : "1.0.5"
}
}
],
"version" : 2
}
30 changes: 30 additions & 0 deletions webauthn-authenticator-rs/src/MacAuthn/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "MacAuthn",
platforms: [
.macOS(.v12)
],
products: [
.library(
name: "MacAuthn",
type: .static,
targets: ["MacAuthn"]),
],
dependencies: [
.package(url: "https://github.com/Brendonovich/swift-rs", from: "1.0.5")
],
targets: [
.target(
name: "MacAuthn",
dependencies: [
.product(
name: "SwiftRs",
package: "swift-rs"
),
]),
]
)
3 changes: 3 additions & 0 deletions webauthn-authenticator-rs/src/MacAuthn/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# MacAuthn

This package is built by `build.rs` using `webauthn-rs`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import AuthenticationServices
import SwiftRs
import Cocoa

enum Result {
case ok([String: Any])
case error(String)
}

class ApplicationDelegate: NSObject, NSApplicationDelegate, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
let window: NSWindow
let authController: ASAuthorizationController
var result: Result = .error("task did not finish")

init(window: NSWindow, authController: ASAuthorizationController) {
self.window = window
self.authController = authController
}

func applicationDidFinishLaunching(_ notification: Notification) {
authController.delegate = self
authController.presentationContextProvider = self
authController.performRequests()
}

func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return window
}

func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if let credential = authorization.credential as? ASAuthorizationSecurityKeyPublicKeyCredentialRegistration {
let rawId = credential.credentialID.toBase64Url()
let clientDataJSON = credential.rawClientDataJSON.toBase64Url()
let attestationObject = credential.rawAttestationObject!.toBase64Url()
self.result = .ok([
"id": rawId,
"rawId": rawId,
"type": "public-key",
"response": [
"clientDataJSON": clientDataJSON,
"attestationObject": attestationObject
]
])
} else if let credential = authorization.credential as? ASAuthorizationSecurityKeyPublicKeyCredentialAssertion {
let signature = credential.signature.toBase64Url()
let clientDataJSON = credential.rawClientDataJSON.toBase64Url()
let authenticatorData = credential.rawAuthenticatorData.toBase64Url()
let rawId = credential.credentialID.toBase64Url()
self.result = .ok([
"id": rawId,
"rawId": rawId,
"type": "public-key",
"response": [
"clientDataJSON": clientDataJSON,
"authenticatorData": authenticatorData,
"signature": signature
]
])
} else if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration {
let rawId = credential.credentialID.toBase64Url()
let clientDataJSON = credential.rawClientDataJSON.toBase64Url()
let attestationObject = credential.rawAttestationObject!.toBase64Url()
self.result = .ok([
"id": rawId,
"rawId": rawId,
"type": "public-key",
"response": [
"clientDataJSON": clientDataJSON,
"attestationObject": attestationObject
]
])
} else if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
let signature = credential.signature.toBase64Url()
let clientDataJSON = credential.rawClientDataJSON.toBase64Url()
let authenticatorData = credential.rawAuthenticatorData.toBase64Url()
let rawId = credential.credentialID.toBase64Url()
self.result = .ok([
"id": rawId,
"rawId": rawId,
"type": "public-key",
"response": [
"clientDataJSON": clientDataJSON,
"authenticatorData": authenticatorData,
"signature": signature
]
])
} else {
self.result = .error("unhandled credential")
}
NSApplication.shared.stop(0)
}

func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
self.result = .error(error.localizedDescription)
NSApplication.shared.stop(0)
}
}

// ASAuthorizationController expects an ASPresentationAnchor (a NSWindow)
// in order to know where to place itself. This function can create such
// a window and run the NSRunLoop event loop to completion for it.
func run(authController: ASAuthorizationController) -> String {
NSApplication.shared.setActivationPolicy(.regular)
let window = NSWindow(contentRect: NSMakeRect(0, 0, 1, 1), styleMask: .borderless, backing: .buffered, defer: false)
window.center()
window.makeKeyAndOrderFront(window)

let applicationDelegate = ApplicationDelegate(window: window, authController: authController)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could this section cause conflicts if there's already a running NSApplication for the process?

NSApplication.shared.delegate = applicationDelegate

NSApplication.shared.activate(ignoringOtherApps: true)
NSApplication.shared.run()

// Rust expects one of either {"data": ...} or {"error": ...}
switch applicationDelegate.result {
case let .ok(data):
return String(data: try! JSONSerialization.data(withJSONObject: ["data": data]), encoding: .utf8)!
case let .error(message):
return String(data: try! JSONSerialization.data(withJSONObject: ["error": message]), encoding: .utf8)!
}
}

@_cdecl("perform_register")
public func performRegister(options: SRString) -> SRString {
let options = try! JSONDecoder().decode(PublicKeyCredentialCreationOptions.self, from: Data(options.toArray()))

let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: options.rp.id)
let platformKeyRequest = platformProvider.createCredentialRegistrationRequest(challenge: options.challenge.decodeBase64Url()!, name: options.user.name, userID: options.user.id.decodeBase64Url()!)
platformKeyRequest.displayName = options.user.displayName
platformKeyRequest.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference.init(rawValue: options.authenticatorSelection.userVerification ?? "preferred")

let securityKeyProvider = ASAuthorizationSecurityKeyPublicKeyCredentialProvider(relyingPartyIdentifier: options.rp.id)

let securityKeyRequest = securityKeyProvider.createCredentialRegistrationRequest(challenge: options.challenge.decodeBase64Url()!, displayName: options.user.displayName, name: options.user.name, userID: options.user.id.decodeBase64Url()!)

securityKeyRequest.credentialParameters = []
for publicKeyParam in options.pubKeyCredParams {
let algorithm = ASCOSEAlgorithmIdentifier(rawValue: publicKeyParam.alg)
let parameters = ASAuthorizationPublicKeyCredentialParameters(algorithm: algorithm)
securityKeyRequest.credentialParameters.append(parameters)
}

securityKeyRequest.excludedCredentials = []
for credential in (options.excludeCredentials ?? []) {
let id = credential.id.decodeBase64Url()!
let transports = credential.transports?.map {
ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor.Transport.init(rawValue: $0)
} ?? ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor.Transport.allSupported
let credential = ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor(credentialID: id, transports: transports)
securityKeyRequest.excludedCredentials.append(credential)
}

securityKeyRequest.attestationPreference = ASAuthorizationPublicKeyCredentialAttestationKind(rawValue: options.attestation ?? "none")
securityKeyRequest.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference.init(rawValue: options.authenticatorSelection.userVerification ?? "preferred")

if options.authenticatorSelection.requireResidentKey == true {
securityKeyRequest.residentKeyPreference = .required
} else {
securityKeyRequest.residentKeyPreference = .preferred
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should use the value of AuthenticatorSelectionCriteria.resident_key; and default to discouraged if require_resident_key = false: https://www.w3.org/TR/webauthn-2/#dom-authenticatorselectioncriteria-residentkey

At the moment, this will always try to use resident keys if a key supports them, potentially filling the authenticator's limited storage with useless RKs.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, this needs to default to discouraged. Because the current code here effectively is forcing "true" on all code paths.

}

let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest, securityKeyRequest])

return SRString(run(authController: authController))
}

@_cdecl("perform_auth")
public func performAuth(options: SRString) -> SRString {
let options = try! JSONDecoder().decode(PublicKeyCredentialRequestOptions.self, from: Data(options.toArray()))

let securityKeyProvider = ASAuthorizationSecurityKeyPublicKeyCredentialProvider(relyingPartyIdentifier: options.rpId)
let securityKeyRequest = securityKeyProvider.createCredentialAssertionRequest(challenge: options.challenge.decodeBase64Url()!)

let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: options.rpId)
let platformKeyRequest = platformProvider.createCredentialAssertionRequest(challenge: options.challenge.decodeBase64Url()!)

securityKeyRequest.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference.init(rawValue: options.userVerification ?? "preferred")

securityKeyRequest.allowedCredentials = []
for credential in (options.allowCredentials ?? []) {
let id = credential.id.decodeBase64Url()!
let transports = credential.transports?.map {
ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor.Transport.init(rawValue: $0)
} ?? ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor.Transport.allSupported
let descriptor = ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor(credentialID: id, transports: transports)
securityKeyRequest.allowedCredentials.append(descriptor)
}
// Setting allowedCredentials can hang for some reason: https://developer.apple.com/forums/thread/727267
securityKeyRequest.allowedCredentials = []

let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest, securityKeyRequest])
Copy link
Collaborator

Choose a reason for hiding this comment

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

Given that ASAuthorizationSecurityKeyPublicKeyCredentialProvider is extremely limited (compared to Windows), I feel like this should be optional (ie: default to both, but use authenticator_attachment to select which backends it tries to use).


return SRString(run(authController: authController))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// File.swift
//
//
// Created by snek on 2023-08-05.
//

import Foundation

struct PublicKeyCredentialDescriptor: Decodable {
let id: String
let transports: [String]?
}

struct PublicKeyCredentialUserEntity: Decodable {
let id: String
let name: String
let displayName: String
}

struct PublicKeyCredentialRpEntity: Decodable {
let id: String
let name: String
}

struct PublicKeyCredentialParameters: Decodable {
let type: String
let alg: Int
}

struct AuthenticatorSelectionCriteria: Decodable {
let userVerification: String?
let requireResidentKey: Bool?
}

struct PublicKeyCredentialCreationOptions: Decodable {
let rp: PublicKeyCredentialRpEntity
let user: PublicKeyCredentialUserEntity
let challenge: String
let pubKeyCredParams: [PublicKeyCredentialParameters]
let timeout: Double
let excludeCredentials: [PublicKeyCredentialDescriptor]?
let authenticatorSelection: AuthenticatorSelectionCriteria
let attestation: String?
}

struct PublicKeyCredentialRequestOptions: Decodable {
let challenge: String
let timeout: Double
let rpId: String
let allowCredentials: [PublicKeyCredentialDescriptor]?
let userVerification: String?
}

extension String {
func decodeBase64Url() -> Data? {
var base64 = self
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
if base64.count % 4 != 0 {
base64.append(String(repeating: "=", count: 4 - base64.count % 4))
}
return Data(base64Encoded: base64)
}
}

extension Data {
func toBase64Url() -> String {
return self
.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
Loading