Skip to content

Commit 2a57b9e

Browse files
refactor: modify JWE to attach apu/apv in base64 decoded format (#112)
* refactor: modify JWE to attach apu/apv in base64 decoded format closes inji/inji-wallet#2288 Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com> * refactor: throw error instead of fallback for mandatory jweheader checks Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com> --------- Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com>
1 parent 992ce0e commit 2a57b9e

10 files changed

Lines changed: 325 additions & 72 deletions

File tree

Sources/OpenID4VP/jwt/JWE/Encryption/types/AESGCMEncryption.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,21 @@ class AESGCMEncryption: JWEEncryption {
99
self.keySize = keySize
1010
}
1111

12-
func encrypt(_ data: Data, with key: SymmetricKey) throws -> (ciphertext: Data, nonce: Data, tag: Data) {
12+
func encrypt(_ data: Data, with key: SymmetricKey, aad: Data? = nil) throws -> (ciphertext: Data, nonce: Data, tag: Data) {
1313

1414
guard key.bitCount == keySize.bitCount else {
1515

1616
throw InvalidEncryptionKeySize(className: AESGCMEncryption.className)
1717
}
1818

1919
let nonce = AES.GCM.Nonce()
20-
let sealedBox = try AES.GCM.seal(data, using: key, nonce: nonce)
20+
let sealedBox: AES.GCM.SealedBox
21+
22+
if let aad = aad {
23+
sealedBox = try AES.GCM.seal(data, using: key, nonce: nonce, authenticating: aad)
24+
} else {
25+
sealedBox = try AES.GCM.seal(data, using: key, nonce: nonce)
26+
}
2127

2228
return (
2329
sealedBox.ciphertext,

Sources/OpenID4VP/jwt/JWE/JWEHandler.swift

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,37 +19,56 @@ public struct JWEHandler {
1919
throw PayloadConversionFailed(className: JWEHandler.className)
2020
}
2121

22-
//TODO: Perform key agreement based on keyEncryptionAlgorithm
2322
let encrypter = try EncryptionProvider.getEncrypter(contentEncryptionAlgorithm)
2423
let keyAgreement = try KeyAgreementFactory.createKeyAgreement(for: publicKey)
25-
let sharedKey = try keyAgreement.deriveKey(publicKey: publicKey.x ?? Data(), algorithm: contentEncryptionAlgorithm, apu: producerInfo, apv: recipientInfo)
24+
25+
guard let publicKeyXCoordinate = publicKey.x else {
26+
throw PublicKeyResolutionFailed(message: "Public key is missing 'x' coordinate", className: Self.className)
27+
}
28+
29+
guard let encryptionAlgorithm = publicKey.algorithm else {
30+
throw PublicKeyResolutionFailed(message: "Public key is missing 'algorithm' property", className: Self.className)
31+
}
2632

27-
let (ciphertext, nonce, tag) = try encrypter.encrypt(payloadData, with: sharedKey)
33+
34+
let sharedKey = try keyAgreement.deriveKey(publicKey: publicKeyXCoordinate, algorithm: contentEncryptionAlgorithm, apu: producerInfo, apv: recipientInfo)
2835

29-
var header = keyAgreement.getJWEHeader(alg: publicKey.algorithm ?? "", enc: contentEncryptionAlgorithm, jwk: publicKey, producerInfo: producerInfo, recipientInfo: recipientInfo)
36+
var header = keyAgreement.getJWEHeader(alg: encryptionAlgorithm, enc: contentEncryptionAlgorithm, jwk: publicKey, producerInfo: producerInfo, recipientInfo: recipientInfo)
3037
if let epk = keyAgreement.getEphemeralPublicKey() {
3138
header["epk"] = epk
3239
}
3340

41+
let encodedHeader = try encodeHeader(header)
42+
let aad = encodedHeader.data(using: .utf8)
43+
44+
let (ciphertext, nonce, tag) = try encrypter.encrypt(payloadData, with: sharedKey, aad: aad)
45+
3446
return try encodeJWEComponents(
35-
header: header,
47+
encodedHeader: encodedHeader,
3648
encryptedKey: keyAgreement.getEncyptionKey(),
3749
nonce: nonce,
3850
ciphertext: ciphertext,
3951
tag: tag
4052
)
4153
}
42-
54+
55+
private func encodeHeader(_ header: [String: Any]) throws -> String {
56+
do {
57+
let headerData = try JSONSerialization.data(withJSONObject: header)
58+
return headerData.toBase64UrlEncoded()
59+
} catch {
60+
throw JsonEncodingFailed(errorMessage: "Failure occurred while encoding the JWE header - \(error)", className: Self.className)
61+
}
62+
}
63+
4364
private func encodeJWEComponents(
44-
header: [String: Any],
65+
encodedHeader: String,
4566
encryptedKey: String,
4667
nonce: Data,
4768
ciphertext: Data,
4869
tag: Data
4970
) throws -> String {
5071

51-
let headerJson = try JSONSerialization.data(withJSONObject: header)
52-
let encodedHeader = headerJson.toBase64UrlEncoded()
5372
let encodedEncryptedKey = encryptedKey
5473
let encodedIV = nonce.toBase64UrlEncoded()
5574
let encodedCiphertext = ciphertext.toBase64UrlEncoded()

Sources/OpenID4VP/jwt/JWE/JWEProtocol/JWEProtocol.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import JSONWebKey
33
import CryptoKit
44

55
protocol JWEKeyAgreement {
6-
func deriveKey(publicKey: Data) throws -> SymmetricKey
76
func deriveKey(publicKey: Data, algorithm: String,
87
apu: String,
98
apv: String) throws -> SymmetricKey
@@ -14,5 +13,5 @@ protocol JWEKeyAgreement {
1413
}
1514

1615
protocol JWEEncryption {
17-
func encrypt(_ data: Data, with key: SymmetricKey) throws -> (ciphertext: Data, nonce: Data, tag: Data)
16+
func encrypt(_ data: Data, with key: SymmetricKey, aad: Data?) throws -> (ciphertext: Data, nonce: Data, tag: Data)
1817
}

Sources/OpenID4VP/jwt/JWE/KeyAgreementFactory/Impl/X25519KeyAgreement.swift

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,6 @@ class X25519KeyAgreement: JWEKeyAgreement {
77
private var ephemeralKeyPair: (privateKey: Curve25519.KeyAgreement.PrivateKey,
88
publicKey: Curve25519.KeyAgreement.PublicKey)?
99

10-
func deriveKey(publicKey: Data) throws -> SymmetricKey {
11-
do {
12-
let privateKey = Curve25519.KeyAgreement.PrivateKey()
13-
let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: publicKey)
14-
15-
ephemeralKeyPair = (privateKey, privateKey.publicKey)
16-
17-
let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: publicKey)
18-
19-
return sharedSecret.hkdfDerivedSymmetricKey(
20-
using: SHA256.self,
21-
salt: "ECDH-ES+A256GCM".data(using: .utf8)!,
22-
sharedInfo: Data(),
23-
outputByteCount: 32
24-
)
25-
} catch {
26-
throw wrapError(
27-
error,
28-
customError: { msg in KeyAgreementFailed(message: msg, className: X25519KeyAgreement.className) }
29-
)
30-
}
31-
}
32-
3310
func deriveKey(publicKey: Data,
3411
algorithm: String = "A256GCM",
3512
apu: String,
@@ -43,9 +20,15 @@ class X25519KeyAgreement: JWEKeyAgreement {
4320
let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: publicKey)
4421

4522
let algorithmID = Data(algorithm.utf8)
46-
let partyUInfo = Data(apu.utf8)
47-
let partyVInfo = Data(apv.utf8)
48-
let keyLength = 32 // 256-bit AES key
23+
24+
guard let partyUInfo = Data(base64UrlEncoded: apu) else {
25+
throw Base64DecodingFailed(message: "Failed to decode producer info (apu)", className: X25519KeyAgreement.className)
26+
}
27+
guard let partyVInfo = Data(base64UrlEncoded: apv) else {
28+
throw Base64DecodingFailed(message: "Failed to decode recipient info (apv)", className: X25519KeyAgreement.className)
29+
}
30+
31+
let keyLength = try getKeyLength(algorithm: algorithm)
4932

5033
// Convert key length (in bits) to big-endian bytes
5134
var bitsBE = UInt32(keyLength * 8).bigEndian
@@ -77,6 +60,15 @@ class X25519KeyAgreement: JWEKeyAgreement {
7760
suppPubInfo: Data,
7861
suppPrivInfo: Data = Data()
7962
) -> SymmetricKey {
63+
var otherInfo = Data()
64+
65+
appendLengthPrefixed(algorithmID, to: &otherInfo)
66+
appendLengthPrefixed(partyUInfo, to: &otherInfo)
67+
appendLengthPrefixed(partyVInfo, to: &otherInfo)
68+
69+
otherInfo.append(suppPubInfo)
70+
otherInfo.append(suppPrivInfo)
71+
8072
var derivedKey = Data()
8173
var counter: UInt32 = 1
8274

@@ -91,11 +83,7 @@ class X25519KeyAgreement: JWEKeyAgreement {
9183

9284
// Append Z and OtherInfo
9385
data.append(zData)
94-
data.append(algorithmID)
95-
data.append(partyUInfo)
96-
data.append(partyVInfo)
97-
data.append(suppPubInfo)
98-
data.append(suppPrivInfo)
86+
data.append(otherInfo)
9987

10088
// Hash
10189
let hash = SHA256.hash(data: data)
@@ -128,4 +116,20 @@ class X25519KeyAgreement: JWEKeyAgreement {
128116
func getEncyptionKey() -> String {
129117
return ""
130118
}
119+
120+
private func appendLengthPrefixed(_ value: Data, to buffer: inout Data) {
121+
var valueLength = UInt32(value.count).bigEndian
122+
buffer.append(Data(bytes: &valueLength, count: 4))
123+
buffer.append(value)
124+
}
125+
126+
private func getKeyLength(algorithm: String) throws -> Int {
127+
let algorithmValue = ContentEncryptionAlgorithm.fromValue(algorithm)
128+
switch algorithmValue {
129+
case .A256GCM:
130+
return 32 // 256-bit AES key
131+
default:
132+
throw UnsupportedOperationException(message: "Unsupported content encryption algorithm: \(algorithm)", className: X25519KeyAgreement.className)
133+
}
134+
}
131135
}

Tests/OpenID4VPTests/AuthorizationResponse/AuthorizationResponseHandlerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ final class AuthorizationResponseHandlerTests: XCTestCase {
66
let state = "state"
77
let responseUri = "https://mock-verifier.com"
88
let holderId = "wallet-holder-id"
9-
let walletNonce = "mock-nonce"
9+
let walletNonce = "_G6UkKgcsUPFlHAbzUMerA"
1010
let signatureSuite = SignatureAlgorithm.ed25519Signature2020.rawValue
1111
let walletMetadata = WalletMetadata()
1212

Tests/OpenID4VPTests/AuthorizationResponse/UnsignedVPToken/UnsignedLdpVPTokenBuilderTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ final class UnsignedLdpVPTokenBuilderTests: XCTestCase {
2626
XCTAssertEqual(token.id, "ebc6f1c2")
2727
XCTAssertEqual(token.holder, "did:example:wallet")
2828
XCTAssertEqual(token.verifiableCredential.count, 1)
29-
assertJsonString(expected: "{\"holder\":\"did:example:wallet\",\"type\":[\"VerifiablePresentation\"],\"@context\":[\"https:\\/\\/www.w3.org\\/2018\\/credentials\\/v1\",\"https:\\/\\/w3id.org\\/security\\/suites\\/ed25519-2020\\/v1\"],\"id\":\"ebc6f1c2\",\"verifiableCredential\":[{\"type\":[\"VerifiableCredential\"],\"issuanceDate\":\"2020-08-19T21:41:50Z\",\"credentialSubject\":{\"id\":\"did:example:subject\"},\"@context\":[\"https:\\/\\/www.w3.org\\/2018\\/credentials\\/v1\"],\"issuer\":\"did:example:issuer\"}],\"proof\":{\"verificationMethod\":\"did:example:wallet\",\"challenge\":\"nonce\",\"domain\":\"client_id\",\"type\":\"Ed25519Signature2020\"}}", actual: (unsignedVPToken as! UnsignedLdpVPToken).dataToSign)
29+
assertJsonString(expected: "{\"holder\":\"did:example:wallet\",\"type\":[\"VerifiablePresentation\"],\"@context\":[\"https:\\/\\/www.w3.org\\/2018\\/credentials\\/v1\",\"https:\\/\\/w3id.org\\/security\\/suites\\/ed25519-2020\\/v1\"],\"id\":\"ebc6f1c2\",\"verifiableCredential\":[{\"type\":[\"VerifiableCredential\"],\"issuanceDate\":\"2020-08-19T21:41:50Z\",\"credentialSubject\":{\"id\":\"did:example:subject\"},\"@context\":[\"https:\\/\\/www.w3.org\\/2018\\/credentials\\/v1\"],\"issuer\":\"did:example:issuer\"}],\"proof\":{\"verificationMethod\":\"did:example:wallet\",\"challenge\":\"tHwahwI6M5_Cd_Sj5k2_Aw\",\"domain\":\"client_id\",\"type\":\"Ed25519Signature2020\"}}", actual: (unsignedVPToken as! UnsignedLdpVPToken).dataToSign)
3030
}
3131

3232
func testCreationOfUnsignedLdpVPTokenWithDifferentSignatureSuite() async throws {
@@ -57,7 +57,7 @@ final class UnsignedLdpVPTokenBuilderTests: XCTestCase {
5757
XCTAssertEqual(token.id, "ebc6f1c2")
5858
XCTAssertEqual(token.holder, "did:example:wallet")
5959
XCTAssertEqual(token.verifiableCredential.count, 1)
60-
assertJsonString(expected: "{\"holder\":\"did:example:wallet\",\"type\":[\"VerifiablePresentation\"],\"@context\":[\"https:\\/\\/www.w3.org\\/2018\\/credentials\\/v1\",\"https:\\/\\/w3id.org\\/security\\/suites\\/jws-2020\\/v1\"],\"id\":\"ebc6f1c2\",\"verifiableCredential\":[{\"type\":[\"VerifiableCredential\"],\"issuanceDate\":\"2020-08-19T21:41:50Z\",\"credentialSubject\":{\"id\":\"did:example:subject\"},\"@context\":[\"https:\\/\\/www.w3.org\\/2018\\/credentials\\/v1\"],\"issuer\":\"did:example:issuer\"}],\"proof\":{\"verificationMethod\":\"did:example:wallet\",\"challenge\":\"nonce\",\"domain\":\"client_id\",\"type\":\"JsonWebSignature2020\"}}", actual: (unsignedVPToken as! UnsignedLdpVPToken).dataToSign)
60+
assertJsonString(expected: "{\"holder\":\"did:example:wallet\",\"type\":[\"VerifiablePresentation\"],\"@context\":[\"https:\\/\\/www.w3.org\\/2018\\/credentials\\/v1\",\"https:\\/\\/w3id.org\\/security\\/suites\\/jws-2020\\/v1\"],\"id\":\"ebc6f1c2\",\"verifiableCredential\":[{\"type\":[\"VerifiableCredential\"],\"issuanceDate\":\"2020-08-19T21:41:50Z\",\"credentialSubject\":{\"id\":\"did:example:subject\"},\"@context\":[\"https:\\/\\/www.w3.org\\/2018\\/credentials\\/v1\"],\"issuer\":\"did:example:issuer\"}],\"proof\":{\"verificationMethod\":\"did:example:wallet\",\"challenge\":\"tHwahwI6M5_Cd_Sj5k2_Aw\",\"domain\":\"client_id\",\"type\":\"JsonWebSignature2020\"}}", actual: (unsignedVPToken as! UnsignedLdpVPToken).dataToSign)
6161
}
6262

6363

Tests/OpenID4VPTests/ResponseModeHandler/types/DirectPostJwtResponseModeHandlerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ final class DirectPostJwtResponseModeHandlerTests: XCTestCase {
590590
mockNetworkManager.setMockResponse(for: responseUri, responseBody: "Response has been shared successfully here.")
591591

592592
let v1Request = getMockAuthorizationRequest(responseMode: .directPostJwt, specVersion: .v1)
593-
let dcqlAuthorizationResult = try await directPostJwtResponseModeHandler.sendAuthorizationResponse(authorizationRequest: v1Request, authorizationResponse: authorizationResponse, url: v1Request.responseUri!, networkManager: mockNetworkManager, producerInfo: "mock-nonce", recipientInfo: "verifier-nonce", walletMetadata: nil)
593+
let dcqlAuthorizationResult = try await directPostJwtResponseModeHandler.sendAuthorizationResponse(authorizationRequest: v1Request, authorizationResponse: authorizationResponse, url: v1Request.responseUri!, networkManager: mockNetworkManager, producerInfo: "tHwahwI6M5_Cd_Sj5k2_Aw", recipientInfo: "_G6UkKgcsUPFlHAbzUMerA", walletMetadata: nil)
594594
let v1RecordedRequest = mockNetworkManager.recordedRequests[responseUri]
595595
XCTAssertEqual(HttpMethod.post, v1RecordedRequest?.requestMethod)
596596
XCTAssertEqual(1, v1RecordedRequest?.requestBody?.keys.count)

Tests/OpenID4VPTests/TestData/TestUtils.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ func getMockAuthorizationRequest(responseMode: ResponseMode = .directPost, respo
253253
responseMode: responseModeValue ?? responseMode.rawValue,
254254
responseUri: "https://mock-verifier.com",
255255
redirectUri: "1234",
256-
nonce: "nonce",
256+
nonce: "tHwahwI6M5_Cd_Sj5k2_Aw",
257257
walletNonce: nil,
258258
state: "state",
259259
presentationDefinition: mockPresentationDefinitionObject,
@@ -267,7 +267,7 @@ func getMockAuthorizationRequest(responseMode: ResponseMode = .directPost, respo
267267
responseMode: responseModeValue ?? responseMode.rawValue,
268268
responseUri: "https://mock-verifier.com",
269269
redirectUri: "1234",
270-
nonce: "nonce",
270+
nonce: "tHwahwI6M5_Cd_Sj5k2_Aw",
271271
walletNonce: nil,
272272
state: "state",
273273
clientMetadata: mockClientMetadataSpecVersion1[responseMode]

0 commit comments

Comments
 (0)