Skip to content

Commit

Permalink
macos authenticator
Browse files Browse the repository at this point in the history
  • Loading branch information
devsnek committed Aug 5, 2023
1 parent d4b4184 commit 746de46
Show file tree
Hide file tree
Showing 12 changed files with 407 additions and 0 deletions.
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
9 changes: 9 additions & 0 deletions webauthn-authenticator-rs/src/MacAuthn/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
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" : "swiftrs",
"kind" : "remoteSourceControl",
"location" : "https://github.com/devsnek/SwiftRs",
"state" : {
"revision" : "a1578d5808b22b5a3461d36a6c8add5cd83a2ddf",
"version" : "1.0.5"
}
}
],
"version" : 2
}
25 changes: 25 additions & 0 deletions webauthn-authenticator-rs/src/MacAuthn/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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/devsnek/SwiftRs", from: "1.0.5")
],
targets: [
.target(
name: "MacAuthn",
dependencies: ["SwiftRs"]),
]
)
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

A description of this package.
193 changes: 193 additions & 0 deletions webauthn-authenticator-rs/src/MacAuthn/Sources/MacAuthn/MacAuthn.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
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)
}
}

// Spawn a mini application to
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)
NSApplication.shared.delegate = applicationDelegate

NSApplication.shared.activate(ignoringOtherApps: true)
// run NSApp event loop. NSApplication.shared.stop(0) above will cause this to return
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
}

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])

return SRString(run(authController: authController))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// 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

0 comments on commit 746de46

Please sign in to comment.