Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,13 @@ public enum IdentityAndAccessManagementMetadata {
return URL(string: metaData.authorizationEndpoint ?? "")
}
}

var tokenEndpointAuthMethods: [String] {
switch self {
case .oidc(let metaData):
return metaData.tokenEndpointAuthMethodsSupported ?? []
case .oauth(let metaData):
return metaData.tokenEndpointAuthMethodsSupported ?? []
}
}
}
30 changes: 30 additions & 0 deletions Sources/Entities/JOSE/SignatureAlgorithm+Extentions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2023 European Commission
*
* 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 Foundation
import JOSESwift

public extension SignatureAlgorithm {
/// Returns `true` if the algorithm is NOT a MAC-based algorithm (HMAC).
var isNotMacAlgorithm: Bool {
switch self {
case .HS256, .HS384, .HS512:
return false // These are HMAC algorithms
default:
return true // All other algorithms are not MAC-based
}
}
}

1 change: 1 addition & 0 deletions Sources/Entities/Types/JWTClaimNames.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ public extension JWTClaimNames {
static let type = "typ"
static let algorithm = "alg"
static let JWK = "jwk"
static let cnf = "cnf"
}
15 changes: 11 additions & 4 deletions Sources/Entities/Wallet/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,28 @@ public typealias ClientId = String
public typealias ClientSecret = String

public struct OpenId4VCIConfig {
public let clientId: ClientId
public let client: Client
public let authFlowRedirectionURI: URL
public let authorizeIssuanceConfig: AuthorizeIssuanceConfig
public let usePAR: Bool
public let dPoPConstructor: DPoPConstructorType?
public let clientAttestationPoPBuilder: ClientAttestationPoPBuilder?


public init(
clientId: ClientId,
client: Client,
authFlowRedirectionURI: URL,
authorizeIssuanceConfig: AuthorizeIssuanceConfig = .favorScopes,
usePAR: Bool = true
usePAR: Bool = true,
dPoPConstructor: DPoPConstructorType? = nil,
clientAttestationPoPBuilder: ClientAttestationPoPBuilder? = nil
) {
self.clientId = clientId
self.client = client
self.authFlowRedirectionURI = authFlowRedirectionURI
self.authorizeIssuanceConfig = authorizeIssuanceConfig
self.usePAR = usePAR
self.dPoPConstructor = dPoPConstructor
self.clientAttestationPoPBuilder = clientAttestationPoPBuilder
}
}

4 changes: 4 additions & 0 deletions Sources/Extensions/Dictionary+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
import Foundation
import SwiftyJSON

public func + (lhs: [String: String], rhs: [String: String]) -> [String: String] {
return lhs.merging(rhs) { (current, new) in new }
}

public extension Dictionary where Key == String, Value == Any {

func toThrowingJSONData() throws -> Data {
Expand Down
75 changes: 50 additions & 25 deletions Sources/Issuers/Issuer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public protocol IssuerType {
func authorizeWithPreAuthorizationCode(
credentialOffer: CredentialOffer,
authorizationCode: IssuanceAuthorization,
clientId: String,
client: Client,
transactionCode: String?,
authorizationDetailsInTokenRequest: AuthorizationDetailsInTokenRequest
) async -> Result<AuthorizedRequest, Error>
Expand Down Expand Up @@ -109,8 +109,12 @@ public actor Issuer: IssuerType {
dpopConstructor: dpopConstructor
)

try config.client.ensureSupportedByAuthorizationServer(
self.authorizationServerMetadata
)

issuanceRequester = IssuanceRequester(
issuerMetadata: issuerMetadata,
issuerMetadata: issuerMetadata,
poster: requesterPoster,
dpopConstructor: dpopConstructor
)
Expand All @@ -132,24 +136,24 @@ public actor Issuer: IssuerType {
let credentials = credentialOffer.credentialConfigurationIdentifiers
let issuerState: String? = switch credentialOffer.grants {
case .authorizationCode(let code),
.both(let code, _):
.both(let code, _):
code.issuerState
default:
nil
}

let (scopes, credentialConfogurationIdentifiers) = try scopesAndCredentialConfigurationIds(credentialOffer: credentialOffer)

let authorizationServerSupportsPar = credentialOffer.authorizationServerMetadata.authorizationServerSupportsPar && config.usePAR

let state = StateValue().value

if authorizationServerSupportsPar {
do {
let resource: String? = issuerMetadata.authorizationServers.map { _ in
credentialOffer.credentialIssuerIdentifier.url.absoluteString
}

let result: (
verifier: PKCEVerifier,
code: GetAuthorizationCodeURL,
Expand Down Expand Up @@ -205,14 +209,14 @@ public actor Issuer: IssuerType {
} catch {
return .failure(ValidationError.error(reason: error.localizedDescription))
}

}
}

public func authorizeWithPreAuthorizationCode(
credentialOffer: CredentialOffer,
authorizationCode: IssuanceAuthorization,
clientId: String,
client: Client,
transactionCode: String?,
authorizationDetailsInTokenRequest: AuthorizationDetailsInTokenRequest = .doNotInclude
) async -> Result<AuthorizedRequest, Error> {
Expand All @@ -221,13 +225,13 @@ public actor Issuer: IssuerType {
case .preAuthorizationCode(let authorisation, let txCode):
do {
if let transactionCode, let txCode {
if txCode.length != transactionCode.count {
throw ValidationError.error(reason: "Expected transaction code length is \(txCode.length ?? 0) but code of length \(transactionCode.count) passed")
}

if txCode.inputMode != .numeric {
throw ValidationError.error(reason: "Issuers expects transaction code to be numeric but is not.")
}
if txCode.length != transactionCode.count {
throw ValidationError.error(reason: "Expected transaction code length is \(txCode.length ?? 0) but code of length \(transactionCode.count) passed")
}
if txCode.inputMode != .numeric {
throw ValidationError.error(reason: "Issuers expects transaction code to be numeric but is not.")
}
}

let credConfigIdsAsAuthDetails: [CredentialConfigurationIdentifier] = switch authorizationDetailsInTokenRequest {
Expand All @@ -238,7 +242,7 @@ public actor Issuer: IssuerType {
let response = try await authorizer.requestAccessTokenPreAuthFlow(
preAuthorizedCode: authorisation,
txCode: txCode,
clientId: clientId,
client: client,
transactionCode: transactionCode,
identifiers: credConfigIdsAsAuthDetails,
dpopNonce: nil,
Expand All @@ -254,14 +258,14 @@ public actor Issuer: IssuerType {
.proofRequired(
accessToken: try .init(
accessToken: accessToken.accessToken,
tokenType: accessToken.tokenType,
tokenType: accessToken.tokenType,
expiresIn: expiresIn?.asTimeInterval ?? .zero
),
refreshToken: try .init(
refreshToken: refreshToken.refreshToken
),
cNonce: cNonce,
credentialIdentifiers: identifiers,
credentialIdentifiers: identifiers,
timeStamp: Date().timeIntervalSinceReferenceDate,
dPopNonce: dPopNonce
)
Expand Down Expand Up @@ -627,7 +631,7 @@ private extension Issuer {
credentialConfigurationIdentifier: CredentialConfigurationIdentifier,
responseEncryptionSpecProvider: (_ issuerResponseEncryptionMetadata: CredentialResponseEncryption) -> IssuanceResponseEncryptionSpec?
) async throws -> Result<SubmittedRequest, Error> {

guard let supportedCredential = issuerMetadata
.credentialsSupported[credentialConfigurationIdentifier] else {
throw ValidationError.error(reason: "Invalid Supported credential for requestSingle")
Expand All @@ -647,7 +651,7 @@ private extension Issuer {
) {
return try supportedCredential.toIssuanceRequest(
requester: issuanceRequester,
claimSet: claimSet,
claimSet: claimSet,
proofs: proofs,
responseEncryptionSpecProvider: responseEncryptionSpecProvider
)
Expand Down Expand Up @@ -732,9 +736,9 @@ public extension Issuer {
try Issuer(
authorizationServerMetadata: .oauth(
.init(
authorizationEndpoint: Constants.url,
tokenEndpoint: Constants.url,
pushedAuthorizationRequestEndpoint: Constants.url
authorizationEndpoint: Constants.url,
tokenEndpoint: Constants.url,
pushedAuthorizationRequestEndpoint: Constants.url
)
),
issuerMetadata: .init(
Expand All @@ -744,7 +748,7 @@ public extension Issuer {
deferredRequesterPoster: deferredRequesterPoster
)
}

static func createResponseEncryptionSpec(_ issuerResponseEncryptionMetadata: CredentialResponseEncryption) -> IssuanceResponseEncryptionSpec? {
switch issuerResponseEncryptionMetadata {
case .notRequired:
Expand Down Expand Up @@ -885,3 +889,24 @@ public extension Issuer {
return .success(authorizedRequest)
}
}

internal extension Client {

private static let ATTEST_JWT_CLIENT_AUTH = "attest_jwt_client_auth"

func ensureSupportedByAuthorizationServer(_ authorizationServerMetadata: IdentityAndAccessManagementMetadata) throws {

let tokenEndpointAuthMethods = authorizationServerMetadata.tokenEndpointAuthMethods

switch self {
case .attested:
let expectedMethod = Self.ATTEST_JWT_CLIENT_AUTH

guard tokenEndpointAuthMethods.contains(expectedMethod) else {
throw ValidationError.error(reason:("\(Self.ATTEST_JWT_CLIENT_AUTH) not supported by authorization server"))
}
default:
break
}
}
}
77 changes: 77 additions & 0 deletions Sources/Main/AttestationBasedClient/Client.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2023 European Commission
*
* 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 Foundation
import JOSESwift

public enum Client {

/// Represents a Public client
case `public`(id: ClientId)

/// Represents an Attested client
case attested(attestationJWT: ClientAttestationJWT, popJwtSpec: ClientAttestationPoPJWTSpec)

// Computed property for 'id' (common property for both cases)
public var id: ClientId {
switch self {
case .public(let id):
return id
case .attested(let attestationJWT, _):
return attestationJWT.clientId
}
}

// MARK: - Validation
public init(public id: ClientId) {
self = .public(id: id)
}

public init(attestationJWT: ClientAttestationJWT, popJwtSpec: ClientAttestationPoPJWTSpec) throws {
// Validate clientId
let clientId = attestationJWT.clientId

guard !clientId.isEmpty && !clientId.trimmingCharacters(in: .whitespaces).isEmpty else {
throw ClientAttestationError.invalidClientId
}

// Validate public key
guard (attestationJWT.pubKey?.isPublicKey ?? false) else {
throw ClientAttestationError.missingJwkClaim
}

self = .attested(attestationJWT: attestationJWT, popJwtSpec: popJwtSpec)
}
}

extension JWK {
/// Determines if the JWK is a private key
var isPrivateKey: Bool {
switch self {
case let rsaKey as RSAPrivateKey:
return !rsaKey.privateExponent.isEmpty
case let ecKey as ECPrivateKey:
return !ecKey.privateKey.isEmpty
default:
return false
}
}

/// Determines if the JWK is a public key
var isPublicKey: Bool {
return !isPrivateKey
}
}

Loading