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

Update to JWTKit 5 #14

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
13 changes: 7 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
// swift-tools-version: 5.8
// swift-tools-version: 6.0
// Copyright (c) 2023 Apple Inc. Licensed under MIT License.

import PackageDescription


let package = Package(
name: "AppStoreServerLibrary",
platforms: [
.macOS(.v11), // And other server environments
.macOS(.v13), // And other server environments
],
products: [
.library(
name: "AppStoreServerLibrary",
targets: ["AppStoreServerLibrary"]),
targets: ["AppStoreServerLibrary"]
),
],
dependencies: [
.package(url: "https://github.com/apple/swift-certificates.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-asn1.git", from: "1.1.0"),
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "4.0.0"),
.package(url: "https://github.com/vapor/jwt-kit.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-nio", from: "2.0.0"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0"),
Expand All @@ -34,7 +34,8 @@ let package = Package(
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "NIOFoundationCompat", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
]),
]
),
.testTarget(
name: "AppStoreServerLibraryTests",
dependencies: ["AppStoreServerLibrary"],
Expand Down
529 changes: 264 additions & 265 deletions Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift

Large diffs are not rendered by default.

162 changes: 60 additions & 102 deletions Sources/AppStoreServerLibrary/ChainVerifier.swift
Original file line number Diff line number Diff line change
@@ -1,141 +1,103 @@
// Copyright (c) 2023 Apple Inc. Licensed under MIT License.

import AsyncHTTPClient
import Foundation
import X509
import SwiftASN1
import JWTKit
import Crypto
import AsyncHTTPClient
import NIOFoundationCompat
import SwiftASN1
import X509

struct ChainVerifier {

private static let EXPECTED_CHAIN_LENGTH = 3
private static let EXPECTED_JWT_SEGMENTS = 3
private static let EXPECTED_ALGORITHM = "ES256"

private let store: CertificateStore

private let verifier: X5CVerifier

init(rootCertificates: [Foundation.Data]) throws {
let parsedCertificates = try rootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) }
self.store = CertificateStore(parsedCertificates)
self.verifier = try X5CVerifier(rootCertificates: rootCertificates)
}

func verify<T: DecodedSignedData>(signedData: String, type: T.Type, onlineVerification: Bool, environment: Environment) async -> VerificationResult<T> where T: Decodable {
let header: JWTHeader;
let decodedBody: T;

func verify<T: DecodedSignedData & JWTPayload>(signedData: String, type: T.Type, onlineVerification: Bool, environment: Environment) async -> VerificationResult<T> where T: Decodable {
let dataToken = Foundation.Data(signedData.utf8)

let (header, payload, _): (header: JWTKit.JWTHeader, payload: T, signature: Foundation.Data)

do {
let bodySegments = signedData.components(separatedBy: ".")
if (bodySegments.count != ChainVerifier.EXPECTED_JWT_SEGMENTS) {
return VerificationResult.invalid(VerificationError.INVALID_JWT_FORMAT)
}
let jsonDecoder = getJsonDecoder()
guard let headerData = Foundation.Data(base64Encoded: base64URLToBase64(bodySegments[0])), let bodyData = Foundation.Data(base64Encoded: base64URLToBase64(bodySegments[1])) else {
return VerificationResult.invalid(VerificationError.INVALID_JWT_FORMAT)
}
header = try jsonDecoder.decode(JWTHeader.self, from: headerData)
decodedBody = try jsonDecoder.decode(type, from: bodyData)
(header, payload, _) = try DefaultJWTParser(jsonDecoder: getJsonDecoder())
.parse(dataToken, as: T.self)
} catch {
return VerificationResult.invalid(VerificationError.INVALID_JWT_FORMAT)
return .invalid(VerificationError.INVALID_JWT_FORMAT)
}
if (environment == Environment.xcode || environment == Environment.localTesting) {

if environment == Environment.xcode || environment == Environment.localTesting {
// Data is not signed by the App Store, and verification should be skipped
// The environment MUST be checked in the public method calling this
return VerificationResult.valid(decodedBody)
return .valid(payload)
}

guard let x5c_header = header.x5c else {
return VerificationResult.invalid(VerificationError.INVALID_JWT_FORMAT)
guard let x5c = header.x5c, x5c.count >= 3 else {
return .invalid(VerificationError.INVALID_JWT_FORMAT)
}
if ChainVerifier.EXPECTED_ALGORITHM != header.alg || x5c_header.count != ChainVerifier.EXPECTED_CHAIN_LENGTH {
return VerificationResult.invalid(VerificationError.INVALID_JWT_FORMAT)
}


guard let leaf_der_enocded = Foundation.Data(base64Encoded: x5c_header[0]),
let intermeidate_der_encoded = Foundation.Data(base64Encoded: x5c_header[1]) else {
return VerificationResult.invalid(VerificationError.INVALID_CERTIFICATE)
}
let validationTime = !onlineVerification && payload.signedDate != nil ? payload.signedDate! : Date()

do {
let leafCertificate = try Certificate(derEncoded: Array(leaf_der_enocded))
let intermediateCertificate = try Certificate(derEncoded: Array(intermeidate_der_encoded))
let validationTime = !onlineVerification && decodedBody.signedDate != nil ? decodedBody.signedDate! : Date()

let verificationResult = await verifyChain(leaf: leafCertificate, intermediate: intermediateCertificate, online: onlineVerification, validationTime: validationTime)
switch verificationResult {
case .validCertificate(let chain):
let leafCertificate = chain.first!
guard let publicKey = P256.Signing.PublicKey(leafCertificate.publicKey) else {
return VerificationResult.invalid(VerificationError.VERIFICATION_FAILURE)
}
// Verify using Vapor
let signers = JWTSigners()
try signers.use(.es256(key: .public(pem: publicKey.pemRepresentation)))
let verifiedBody: VerificationResult<VaporBody> = try VerificationResult<VaporBody>.valid(signers.verify(signedData))
switch verifiedBody {
case .invalid(_):
return VerificationResult.invalid(VerificationError.VERIFICATION_FAILURE)
case .valid(_): break
let result = try await verifier.verifyJWS(dataToken, as: T.self, jsonDecoder: getJsonDecoder(), policy: {
RFC5280Policy(validationTime: validationTime)
AppStoreOIDPolicy()
if onlineVerification {
OCSPVerifierPolicy(failureMode: .hard, requester: Requester(), validationTime: Date())
}
return VerificationResult.valid(decodedBody)
case .couldNotValidate(_):
return VerificationResult.invalid(VerificationError.VERIFICATION_FAILURE)
}
})
return .valid(result)
} catch {
return VerificationResult.invalid(VerificationError.INVALID_JWT_FORMAT)
}
}

func verifyChain(leaf: Certificate, intermediate: Certificate, online: Bool, validationTime: Date) async -> X509.VerificationResult {
var verifier = Verifier(rootCertificates: self.store) {
RFC5280Policy(validationTime: validationTime)
AppStoreOIDPolicy()
if online {
OCSPVerifierPolicy(failureMode: .hard, requester: Requester(), validationTime: Date())
if
let jwtError = error as? JWTError,
jwtError.errorType == .missingX5CHeader || jwtError.errorType == .malformedToken
{
return .invalid(VerificationError.INVALID_JWT_FORMAT)
} else {
return .invalid(VerificationError.VERIFICATION_FAILURE)
}
}
let intermediateStore = CertificateStore([intermediate])
return await verifier.validate(leafCertificate: leaf, intermediates: intermediateStore)
}
}

class VaporBody : JWTPayload {
func verify(using signer: JWTKit.JWTSigner) throws {
// No-op
func verifyChain(leaf: Certificate, intermediate: Certificate, online: Bool, validationTime: Date) async -> X509.VerificationResult {
do {
return try await verifier.verifyChain(certificates: [leaf, intermediate], policy: {
RFC5280Policy(validationTime: validationTime)
AppStoreOIDPolicy()
if online {
OCSPVerifierPolicy(failureMode: .hard, requester: Requester(), validationTime: Date())
}
})
} catch {
return .couldNotValidate([])
}
}
}

struct JWTHeader: Decodable, Encodable {
public var alg: String?
public var x5c: [String]?
}

final class AppStoreOIDPolicy: VerifierPolicy {

private static let NUMBER_OF_CERTS = 3
private static let WWDR_INTERMEDIATE_OID: ASN1ObjectIdentifier = [1, 2, 840, 113635, 100, 6, 2, 1]
private static let RECEIPT_SIGNER_OID: ASN1ObjectIdentifier = [1, 2, 840, 113635, 100, 6, 11, 1]
private static let WWDR_INTERMEDIATE_OID: ASN1ObjectIdentifier = [1, 2, 840, 113_635, 100, 6, 2, 1]
private static let RECEIPT_SIGNER_OID: ASN1ObjectIdentifier = [1, 2, 840, 113_635, 100, 6, 11, 1]

init() {
verifyingCriticalExtensions = []
}

var verifyingCriticalExtensions: [SwiftASN1.ASN1ObjectIdentifier]

func chainMeetsPolicyRequirements(chain: X509.UnverifiedCertificateChain) async -> X509.PolicyEvaluationResult {
if (chain.count != AppStoreOIDPolicy.NUMBER_OF_CERTS) {
if chain.count != AppStoreOIDPolicy.NUMBER_OF_CERTS {
return X509.PolicyEvaluationResult.failsToMeetPolicy(reason: "Chain has unexpected length")
}
let intermediateCertificate = chain[1]
let leafCertificate = chain[0]
if (!intermediateCertificate.extensions.contains(where: { ext in
if !intermediateCertificate.extensions.contains(where: { ext in
ext.oid == AppStoreOIDPolicy.WWDR_INTERMEDIATE_OID
})) {
}) {
return X509.PolicyEvaluationResult.failsToMeetPolicy(reason: "Intermediate certificate does not contain WWDR OID")
}
if (!leafCertificate.extensions.contains(where: { ext in
if !leafCertificate.extensions.contains(where: { ext in
ext.oid == AppStoreOIDPolicy.RECEIPT_SIGNER_OID
})) {
}) {
return X509.PolicyEvaluationResult.failsToMeetPolicy(reason: "Leaf certificate does not contain Receipt Signing OID")
}
return X509.PolicyEvaluationResult.meetsPolicy
Expand All @@ -145,15 +107,11 @@ final class AppStoreOIDPolicy: VerifierPolicy {
final class Requester: OCSPRequester {
func query(request: [UInt8], uri: String) async -> X509.OCSPRequesterQueryResult {
do {
let httpClient = HTTPClient()
defer {
try? httpClient.syncShutdown()
}
var urlRequest = HTTPClientRequest(url: uri)
urlRequest.method = .POST
urlRequest.headers.add(name: "Content-Type", value: "application/ocsp-request")
urlRequest.body = .bytes(request)
let response = try await httpClient.execute(urlRequest, timeout: .seconds(30))
let response = try await HTTPClient.shared.execute(urlRequest, timeout: .seconds(30))
var body = try await response.body.collect(upTo: 1024 * 1024)
guard let data = body.readData(length: body.readableBytes) else {
throw OCSPFetchError()
Expand All @@ -163,7 +121,7 @@ final class Requester: OCSPRequester {
return .terminalError(error)
}
}

private struct OCSPFetchError: Error {}
}

Expand Down
48 changes: 34 additions & 14 deletions Sources/AppStoreServerLibrary/SignedDataVerifier.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Copyright (c) 2023 Apple Inc. Licensed under MIT License.

import Foundation
import JWTKit

///A verifier and decoder class designed to decode signed data from the App Store.
/// A verifier and decoder class designed to decode signed data from the App Store.
public struct SignedDataVerifier {

public enum ConfigurationError: Error {
Expand All @@ -14,7 +15,7 @@ public struct SignedDataVerifier {
private var environment: Environment
private var chainVerifier: ChainVerifier
private var enableOnlineChecks: Bool

/// - Parameter rootCertificates: The set of Apple Root certificate authority certificates, as found on [Apple PKI](https://www.apple.com/certificateauthority/)
/// - Parameter bundleId: The: bundle identifier of the app.
/// - Parameter appAppleId: The unique identifier of the app in the App Store.
Expand All @@ -33,6 +34,7 @@ public struct SignedDataVerifier {
self.chainVerifier = try ChainVerifier(rootCertificates: rootCertificates)
self.enableOnlineChecks = enableOnlineChecks
}

/// Verifies and decodes a signedRenewalInfo obtained from the App Store Server API, an App Store Server Notification, or from a device
/// See [JWSRenewalInfo](https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo)
///
Expand All @@ -41,15 +43,16 @@ public struct SignedDataVerifier {
public func verifyAndDecodeRenewalInfo(signedRenewalInfo: String) async -> VerificationResult<JWSRenewalInfoDecodedPayload> {
let renewalInfoResult = await decodeSignedData(signedData: signedRenewalInfo, type: JWSRenewalInfoDecodedPayload.self)
switch renewalInfoResult {
case .valid(let renewalInfo):
case let .valid(renewalInfo):
if self.environment != renewalInfo.environment {
return VerificationResult.invalid(VerificationError.INVALID_ENVIRONMENT)
}
case .invalid(_):
case .invalid:
break
}
return renewalInfoResult
}

/// Verifies and decodes a signedTransaction obtained from the App Store Server API, an App Store Server Notification, or from a device
/// See [JWSTransaction](https://developer.apple.com/documentation/appstoreserverapi/jwstransaction)
///
Expand All @@ -58,18 +61,19 @@ public struct SignedDataVerifier {
public func verifyAndDecodeTransaction(signedTransaction: String) async -> VerificationResult<JWSTransactionDecodedPayload> {
let transactionResult = await decodeSignedData(signedData: signedTransaction, type: JWSTransactionDecodedPayload.self)
switch transactionResult {
case .valid(let transaction):
case let .valid(transaction):
if self.bundleId != transaction.bundleId {
return VerificationResult.invalid(VerificationError.INVALID_APP_IDENTIFIER)
}
if self.environment != transaction.environment {
return VerificationResult.invalid(VerificationError.INVALID_ENVIRONMENT)
}
case .invalid(_):
case .invalid:
break
}
return transactionResult
}

/// Verifies and decodes an App Store Server Notification signedPayload
/// See [signedPayload](https://developer.apple.com/documentation/appstoreservernotifications/signedpayload)
///
Expand Down Expand Up @@ -110,7 +114,7 @@ public struct SignedDataVerifier {
if let result = validateNotification(bundleId, appAppleId, environment) {
return .invalid(result)
}
case .invalid(_):
case .invalid:
break
}
return notificationResult
Expand All @@ -129,26 +133,42 @@ public struct SignedDataVerifier {
///Verifies and decodes a signed AppTransaction
///See [AppTransaction](https://developer.apple.com/documentation/storekit/apptransaction)
///
///- Parameter signedAppTransaction The signed AppTransaction
///- Returns: If success, the decoded AppTransaction after validation, else the reason for verification failure
/// - Parameter signedAppTransaction The signed AppTransaction
/// - Returns: If success, the decoded AppTransaction after validation, else the reason for verification failure
public func verifyAndDecodeAppTransaction(signedAppTransaction: String) async -> VerificationResult<AppTransaction> {
let appTransactionResult = await decodeSignedData(signedData: signedAppTransaction, type: AppTransaction.self)
switch appTransactionResult {
case .valid(let appTransaction):
case let .valid(appTransaction):
let environment = appTransaction.receiptType
if self.bundleId != appTransaction.bundleId || (self.environment == .production && self.appAppleId != appTransaction.appAppleId) {
return VerificationResult.invalid(VerificationError.INVALID_APP_IDENTIFIER)
}
if self.environment != environment {
return VerificationResult.invalid(VerificationError.INVALID_ENVIRONMENT)
}
case .invalid(_):
case .invalid:
break
}
return appTransactionResult
}
private func decodeSignedData<T: DecodedSignedData>(signedData: String, type: T.Type) async -> VerificationResult<T> where T : Decodable {
return await chainVerifier.verify(signedData: signedData, type: type, onlineVerification: self.enableOnlineChecks, environment: self.environment)

private func decodeSignedData<T: DecodedSignedData & JWTPayload>(signedData: String, type: T.Type) async -> VerificationResult<T> where T: Decodable {
await chainVerifier.verify(signedData: signedData, type: type, onlineVerification: self.enableOnlineChecks, environment: self.environment)
}
}

extension AppTransaction: JWTPayload {
public func verify(using algorithm: some JWTAlgorithm) async throws {}
}

extension ResponseBodyV2DecodedPayload: JWTPayload {
public func verify(using algorithm: some JWTAlgorithm) async throws {}
}

extension JWSTransactionDecodedPayload: JWTPayload {
public func verify(using algorithm: some JWTAlgorithm) async throws {}
}

extension JWSRenewalInfoDecodedPayload: JWTPayload {
public func verify(using algorithm: some JWTAlgorithm) async throws {}
}
Loading