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

Version 1.0.0 #1

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
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 .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Sources/CNemIDBoringSSL/* linguist-vendored
Sources/Clibxml2/* linguist-vendored
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ jobs:
- swift:5.3-focal
container: ${{ matrix.image }}
steps:
- name: Update apt
run: apt update -y
- name: Install dependencies
run: apt install -y libxml2-dev
- name: Checkout the code
uses: actions/checkout@v2
- name: Run tests with Thread sanitizer
run: swift test --eanble-test-discovery --sanitize=thread
run: swift test --enable-test-discovery --sanitize=thread
macOS:
runs-on: macos-latest
steps:
Expand Down
42 changes: 30 additions & 12 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
"repositoryURL": "https://github.com/swift-server/async-http-client.git",
"state": {
"branch": null,
"revision": "ba845ee93d6ea10671e829ebf273b6f7a0c92ef0",
"version": "1.2.4"
"revision": "1081b0b0541f535ca088acdb56f5ca5598bc6247",
"version": "1.6.3"
}
},
{
"package": "swift-crypto",
"repositoryURL": "https://github.com/apple/swift-crypto.git",
"state": {
"branch": null,
"revision": "296d3308b4b2fa355cfe0de4ca411bf7a1cd8cf8",
"version": "1.1.4"
"revision": "3bea268b223651c4ab7b7b9ad62ef9b2d4143eb6",
"version": "1.1.6"
}
},
{
Expand All @@ -33,35 +33,53 @@
"repositoryURL": "https://github.com/apple/swift-nio.git",
"state": {
"branch": null,
"revision": "6d3ca7e54e06a69d0f2612c2ce8bb8b7319085a4",
"version": "2.26.0"
"revision": "6aa9347d9bc5bbfe6a84983aec955c17ffea96ef",
"version": "2.33.0"
}
},
{
"package": "swift-nio-extras",
"repositoryURL": "https://github.com/apple/swift-nio-extras.git",
"state": {
"branch": null,
"revision": "de1c80ad1fdff1ba772bcef6b392c3ef735f39a6",
"version": "1.8.0"
"revision": "f73ca5ee9c6806800243f1ac415fcf82de9a4c91",
"version": "1.10.2"
}
},
{
"package": "swift-nio-http2",
"repositoryURL": "https://github.com/apple/swift-nio-http2.git",
"state": {
"branch": null,
"revision": "326f7f9a8c8c8402e3691adac04911cac9f9d87f",
"version": "1.18.4"
}
},
{
"package": "swift-nio-ssl",
"repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
"state": {
"branch": null,
"revision": "bbb38fbcbbe9dc4665b2c638dfa5681b01079bfb",
"version": "2.10.4"
"revision": "5e68c1ded15619bb281b273fa8c2d8fd7f7b2b7d",
"version": "2.16.1"
}
},
{
"package": "swift-nio-transport-services",
"repositoryURL": "https://github.com/apple/swift-nio-transport-services.git",
"state": {
"branch": null,
"revision": "1d28d48e071727f4558a8a4bb1894472abc47a58",
"version": "1.9.2"
"revision": "e7f5278a26442dc46783ba7e063643d524e414a0",
"version": "1.11.3"
}
},
{
"package": "XMLCoder",
"repositoryURL": "https://github.com/MaxDesiatov/XMLCoder.git",
"state": {
"branch": null,
"revision": "887de88b37b2d691d67db950770e09776229cf6d",
"version": "0.13.0"
}
}
]
Expand Down
11 changes: 9 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.2
// swift-tools-version:5.3
import PackageDescription

let package = Package(
Expand All @@ -15,8 +15,11 @@ let package = Package(
MANGLE_END */
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.33.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.16.1"),
.package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.3"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.3.0"),
.package(url: "https://github.com/MaxDesiatov/XMLCoder.git", from: "0.12.0"),
],
targets: [
.target(
Expand All @@ -25,7 +28,11 @@ let package = Package(
"Clibxml2",
"CNemIDBoringSSL",
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOFoundationCompat", package: "swift-nio"),
.product(name: "NIOSSL", package: "swift-nio-ssl"),
.product(name: "AsyncHTTPClient", package: "async-http-client"),
"XMLCoder",
]
),
.target(name: "CNemIDBoringSSL"),
Expand Down
50 changes: 14 additions & 36 deletions Sources/NemID/Client Parameters/ClientParameters.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import Foundation

public enum NemIDClientParametersClientFlow: String, Encodable {
public enum NemIDClientParametersClientFlow: String, Codable {
/// 2 factor OCES login
case ocesLogin2
case ocesLogin2 = "OCESLOGIN2"
}

public enum NemIDClientParametersClientLanguage: String, Encodable {
public enum NemIDClientParametersClientLanguage: String, Codable {
case danish = "DA"
case english = "EN"
case greenlandic = "KL"
Expand All @@ -14,52 +14,30 @@ public enum NemIDClientParametersClientLanguage: String, Encodable {
public protocol NemIDClientParameters {
/// Determines which NemID flow to start
var clientFlow: NemIDClientParametersClientFlow { get }

/// Client language
var language: NemIDClientParametersClientLanguage { get }

/// The origin of the Service Provider site which will send parameters to the NemID JavaScript client.
/// The NemID JavaScript client will abort with APP001 or APP007 if a postMessage command is received from any other origin.
var origin: URL? { get }

/// Base64 encoded token returned from the client when the user chooses to remember his user id.
/// At next login/signing this parameter must be specified in order to enable the remember user id functionality.
var rememberUserID: String? { get }

/// Indicates that the “Remember userid checkbox” should not be initially checked.
/// This is only relevant in responsive mode and when REMEMBER_USERID is also set.
var rememberUserIDInitialStatus: Bool? { get }
/// Base64 encoded DER representation of the certificate used for identifying the OCES Service Provider
var SPCert: String { get }

/// Current time when generating parameters. The timestamp parameter is converted to UTC and must match the NemID server time.
/// NemID accepts timestamps within the boundaries of +-3 minutes.
var timestamp: Date { get }
}

// MARK: - NemIDClientParameters+Normalized
extension NemIDClientParameters {
/// Returns a normalized string of the parameters, as described in the NemID documentation.

/// Indicates if the JS an awaiting app client should send out app approval event
/// The event can be caught outside the JS client using the event handler set-up from section 3.3 where command will be “AwaitingAppApproval
///
/// 1. The parameters are sorted alphabetically by name. The sorting is case-insensitive.
/// 2. Each parameter is concatenated to the result string as an alternating sequence of name and value
var normalized: String {
var parameters: [String: String] = [
"CLIENTFLOW": self.clientFlow.rawValue,
"LANGUAGE": self.language.rawValue,
"SP_CERT": self.SPCert,
"TIMESTAMP": String(self.timestamp.timeIntervalSince1970 * 1000)
]

if let origin = self.origin?.absoluteString {
parameters["ORIGIN"] = origin
}
if let rememberUserID = self.rememberUserID {
parameters["REMEMBER_USERID"] = rememberUserID
}
if let rememberUserIDInitialStatus = self.rememberUserIDInitialStatus {
parameters["REMEMBER_USERID_INITIAL_STATUS"] = rememberUserIDInitialStatus ? "TRUE": "FALSE"
}

let sortedAlphabeticallyByKeys = parameters.sorted(by: { $0.key.lowercased() < $1.key.lowercased()} )
return sortedAlphabeticallyByKeys
.reduce(into: "") { result, parameter in
result += "\(parameter.key)\(parameter.value)"
}
}
/// TRUE => Enable awaiting client should send out app approval event.
/// Any other value or unset (default) => No event
var enableAwaitingAppApprovalEvent: Bool? { get }
}
60 changes: 50 additions & 10 deletions Sources/NemID/Client Parameters/ParametersSigner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,73 @@ enum NemIDParameterSigningError: Error {
case invalidData
}

public struct NemIDParametersSigner {
private let rsaSigner: RSASigner
struct NemIDParametersSigner {
private let configuration: NemIDConfiguration

public init(rsaSigner: RSASigner) {
self.rsaSigner = rsaSigner
public init(configuration: NemIDConfiguration) {
self.configuration = configuration
}

/// Follows steps according to NemID documentation 3.2
public func sign(_ parameters: NemIDUnsignedClientParameters) throws -> NemIDSignedClientParameters {
let normalizedData = [UInt8](parameters.normalized.utf8)
// Step 1:
let normalizedData = try [UInt8](normalizedParameters(parameters).utf8)
let digest = SHA256.hash(data: normalizedData)
// RSASigner also SHA256 hashes the data.
let signature = try rsaSigner.sign(normalizedData)

// Step 2:
let signer = RSASigner(key: configuration.privateKey)
let signature = try signer.sign(normalizedData)

// Step 3:
let base64ParamsDigest = Data(digest).base64EncodedString()
let base64SignedDigest = Data(signature).base64EncodedString()

return NemIDSignedClientParameters(
// Step 4:
return try NemIDSignedClientParameters(
clientFlow: parameters.clientFlow,
language: parameters.language,
origin: parameters.origin,
rememberUserID: parameters.rememberUserID,
rememberUserIDInitialStatus: parameters.rememberUserIDInitialStatus,
SPCert: parameters.SPCert,
spCert: configuration.spCertificate.toBase64EncodedDER(),
timestamp: parameters.timestamp,
digestSignature: base64SignedDigest,
paramsDigest: base64ParamsDigest
paramsDigest: base64ParamsDigest,
enableAwaitingAppApprovalEvent: parameters.enableAwaitingAppApprovalEvent
)
}

/// Returns a normalized string of the parameters, as described in the NemID documentation.
///
/// 1. The parameters are sorted alphabetically by name. The sorting is case-insensitive.
/// 2. Each parameter is concatenated to the result string as an alternating sequence of name and value
private func normalizedParameters(_ unsignedParameters: NemIDUnsignedClientParameters) throws -> String {
var parameters: [String: String] = try [
"CLIENTFLOW": unsignedParameters.clientFlow.rawValue,
"LANGUAGE": unsignedParameters.language.rawValue,
"SP_CERT": configuration.spCertificate.toBase64EncodedDER(),
"TIMESTAMP": String(Int(unsignedParameters.timestamp.timeIntervalSince1970 * 1000))
]

if let origin = unsignedParameters.origin?.absoluteString {
parameters["ORIGIN"] = origin
}
if let rememberUserID = unsignedParameters.rememberUserID {
parameters["REMEMBER_USERID"] = rememberUserID
}
if let rememberUserIDInitialStatus = unsignedParameters.rememberUserIDInitialStatus {
parameters["REMEMBER_USERID_INITIAL_STATUS"] = rememberUserIDInitialStatus ? "TRUE": "FALSE"
}

if let enableAwaitingAppApprovalEvent = unsignedParameters.enableAwaitingAppApprovalEvent {
parameters["ENABLE_AWAITING_APP_APPROVAL_EVENT"] = enableAwaitingAppApprovalEvent ? "TRUE" : "FALSE"
}

let sortedAlphabeticallyByKeys = parameters.sorted(by: { $0.key.lowercased() < $1.key.lowercased()} )
return sortedAlphabeticallyByKeys
.reduce(into: "") { result, parameter in
result += "\(parameter.key)\(parameter.value)"
}
}
}

18 changes: 11 additions & 7 deletions Sources/NemID/Client Parameters/SignedClientParameters.swift
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
import Foundation

/// This is the type which the SP-server will send to NemID client as JSON.
public struct NemIDSignedClientParameters: NemIDClientParameters, Encodable {
public struct NemIDSignedClientParameters: NemIDClientParameters, Codable {
public let clientFlow: NemIDClientParametersClientFlow
public let language: NemIDClientParametersClientLanguage
public let origin: URL?
public let rememberUserID: String?
public let rememberUserIDInitialStatus: Bool?
public let SPCert: String
public let spCert: String
public let timestamp: Date
/// Base64 encoded RSA256 signature of the calculated parameter digest.
public let digestSignature: String
/// Base64 encoded representation of the calculated parameter digest.
public let paramsDigest: String
public let enableAwaitingAppApprovalEvent: Bool?

enum CodingKeys: String, CodingKey {
case clientFlow = "CLIENTFLOW"
case language = "LANGUAGE"
case origin = "ORIGIN"
case rememberUserID = "REMEMBER_USERID"
case rememberUserIDInitialStatus = "REMEMBER_USERID_INITIAL_STATUS"
case SPCert = "SP_CERT"
case spCert = "SP_CERT"
case timestamp = "TIMESTAMP"
case digestSignature = "DIGEST_SIGNATURE"
case paramsDigest = "PARAMS_DIGEST"
case enableAwaitingAppApprovalEvent = "ENABLE_AWAITING_APP_APPROVAL_EVENT"
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(clientFlow, forKey: .clientFlow)
try container.encode(language, forKey: .language)
try container.encode(origin, forKey: .origin)
try container.encode(rememberUserID, forKey: .rememberUserID)
try container.encode(rememberUserIDInitialStatus, forKey: .rememberUserIDInitialStatus)
try container.encode(timestamp.timeIntervalSince1970 * 1000, forKey: .timestamp)
try container.encodeIfPresent(origin, forKey: .origin)
try container.encodeIfPresent(rememberUserID, forKey: .rememberUserID)
try container.encodeIfPresent(rememberUserIDInitialStatus?.nemIDRepresentation, forKey: .rememberUserIDInitialStatus)
try container.encode(spCert, forKey: .spCert)
try container.encode(String(Int(timestamp.timeIntervalSince1970 * 1000)), forKey: .timestamp)
try container.encode(digestSignature, forKey: .digestSignature)
try container.encode(paramsDigest, forKey: .paramsDigest)
try container.encodeIfPresent(enableAwaitingAppApprovalEvent?.nemIDRepresentation, forKey: .enableAwaitingAppApprovalEvent)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@ public struct NemIDUnsignedClientParameters: NemIDClientParameters {
public let origin: URL?
public let rememberUserID: String?
public let rememberUserIDInitialStatus: Bool?
public let SPCert: String
public let timestamp: Date
public let enableAwaitingAppApprovalEvent: Bool?

public init(
clientFlow: NemIDClientParametersClientFlow,
language: NemIDClientParametersClientLanguage,
origin: URL?,
rememberUserID: String?,
rememberUserIDInitialStatus: Bool?,
SPCert: String,
timestamp: Date
timestamp: Date,
enableAwaitingAppApprovalEvent: Bool?
) {
self.clientFlow = clientFlow
self.language = language
self.origin = origin
self.rememberUserID = rememberUserID
self.rememberUserIDInitialStatus = rememberUserIDInitialStatus
self.SPCert = SPCert
self.timestamp = timestamp
self.enableAwaitingAppApprovalEvent = enableAwaitingAppApprovalEvent
}
}
Loading