@@ -33,8 +33,8 @@ import Foundation
3333/// // Handle error
3434/// }
3535///
36- /// // 3. Initialize ODoH routine with selected configuration
37- /// let clientRoutine = try ODoH.Routine (configuration: selectedConfig)
36+ /// // 3. Initialize ODoH client routine with selected configuration
37+ /// let clientRoutine = try ODoH.ClientRoutine (configuration: selectedConfig)
3838///
3939/// // 4. Create DNS query with padding for privacy
4040/// let dnsQuery: Data = buildDNSQuery(domain: "example.com", type: .A)
@@ -59,7 +59,7 @@ import Foundation
5959/// // 1. Server creates and publishes configuration
6060/// let serverPrivateKey = Curve25519.KeyAgreement.PrivateKey()
6161/// let serverConfig = try ODoH.Configuration.v1(privateKey: serverPrivateKey)
62- /// let serverRoutine = try ODoH.Routine (configuration: serverConfig)
62+ /// let serverRoutine = try ODoH.ServerRoutine (configuration: serverConfig)
6363///
6464/// // 2. Publish configuration at well-known endpoint
6565/// let configsToPublish: [ODoH.Configuration] = [serverConfig]
@@ -85,14 +85,14 @@ import Foundation
8585/// // 6. Send encrypted response back through proxy to client...
8686/// ```
8787public enum ODoH : Sendable {
88- /// ODoH cryptographic routine for encrypting queries and decrypting responses .
88+ /// Shared helpers for ODoH cryptographic routines .
8989 ///
90- /// Handles the client-side and server-side cryptographic operations for Oblivious DNS over HTTPS .
90+ /// Contains common functionality for both client and server ODoH operations.
9191 /// Initialized with a configuration containing the target's public key and algorithm parameters.
92- public struct Routine {
93- private var ct : HPKE . Ciphersuite
94- private var pkR : any HPKEDiffieHellmanPublicKey
95- private var keyID : Data
92+ internal struct RoutineCore {
93+ internal var ct : HPKE . Ciphersuite
94+ internal var pkR : any HPKEDiffieHellmanPublicKey
95+ internal var keyID : Data
9696
9797 /// Initialize ODoH encryption with target server configuration.
9898 ///
@@ -101,7 +101,7 @@ public enum ODoH: Sendable {
101101 /// HKDF as specified in RFC 9230 Section 6.2.
102102 ///
103103 /// - Parameter configuration: Target server's ODoH configuration
104- public init ( configuration: Configuration ) throws {
104+ internal init ( configuration: Configuration ) throws {
105105 guard configuration. contents. aead != . exportOnly else {
106106 throw ObliviousDoHError . unsupportedHPKEParameters
107107 }
@@ -115,6 +115,83 @@ public enum ODoH: Sendable {
115115 self . keyID = configuration. contents. identifier
116116 }
117117
118+ /// Derive AEAD key and nonce for response encryption.
119+ ///
120+ /// Uses HKDF Extract-and-Expand with query plaintext and response nonce as salt.
121+ /// Formula: Extract(Q_plain || len(nonce) || nonce, secret) → Expand for key/nonce.
122+ ///
123+ /// - Parameters:
124+ /// - secret: Exported secret from HPKE context
125+ /// - queryPlain: Original query plaintext
126+ /// - responseNonce: Server-generated nonce
127+ /// - Returns: Derived AEAD key and nonce
128+ internal func deriveSecrets(
129+ secret: SymmetricKey ,
130+ queryPlain: Data ,
131+ responseNonce: Data
132+ ) throws -> ( key: SymmetricKey , nonce: Data ) {
133+ // Build salt: Q_plain || len(resp_nonce) || resp_nonce
134+ var salt = Data ( )
135+ salt. reserveCapacity ( queryPlain. count + 2 + responseNonce. count)
136+ salt. append ( queryPlain)
137+ salt. append ( bigEndianBytes: UInt16 ( responseNonce. count) )
138+ salt. append ( responseNonce)
139+
140+ // Extract PRK
141+ let prk = self . ct. kdf. extract ( salt: salt, ikm: secret)
142+
143+ // Expand to get key and nonce
144+ let key = self . ct. kdf. expand (
145+ prk: prk,
146+ info: Data . oDoHKeyInfo,
147+ outputByteCount: self . ct. aead. keyByteCount
148+ )
149+ let nonce = self . ct. kdf. expand (
150+ prk: prk,
151+ info: Data . oDoHNonceInfo,
152+ outputByteCount: self . ct. aead. nonceByteCount
153+ )
154+
155+ return ( key, Data ( nonce) )
156+ }
157+
158+ /// Construct Additional Authenticated Data (AAD) for AEAD operations.
159+ ///
160+ /// Format: message_type (1 byte) || key_length (2 bytes) || key_data
161+ ///
162+ /// - Parameters:
163+ /// - type: Message type (query or response)
164+ /// - key: Key identifier or response nonce
165+ /// - Returns: AAD bytes for AEAD operations
166+ internal func aad( _ type: Message . MessageType , key: Data ) -> Data {
167+ var aad = Data ( )
168+ let keyLength = UInt16 ( key. count)
169+ aad. reserveCapacity ( 1 + 2 + key. count)
170+ aad. append ( type. rawValue)
171+ aad. append ( bigEndianBytes: keyLength)
172+ aad. append ( key)
173+ return aad
174+ }
175+ }
176+
177+ /// ODoH client routine for encrypting queries and decrypting responses.
178+ ///
179+ /// Handles the client-side cryptographic operations for Oblivious DNS over HTTPS.
180+ /// Initialized with a configuration containing the target's public key and algorithm parameters.
181+ public struct ClientRoutine {
182+ private var core : RoutineCore
183+
184+ /// Initialize ODoH client routine with target server configuration.
185+ ///
186+ /// Extracts the HPKE ciphersuite parameters and derives the key identifier
187+ /// from the provided configuration. The key identifier is computed using
188+ /// HKDF as specified in RFC 9230 Section 6.2.
189+ ///
190+ /// - Parameter configuration: Target server's ODoH configuration
191+ public init ( configuration: Configuration ) throws {
192+ self . core = try RoutineCore ( configuration: configuration)
193+ }
194+
118195 /// Encrypt DNS query using HPKE for transmission through proxy.
119196 ///
120197 /// Returns encrypted message and sender context needed for response decryption.
@@ -125,14 +202,14 @@ public enum ODoH: Sendable {
125202 queryPlain: MessagePlaintext
126203 ) throws -> QueryEncryptionResult {
127204 var context = try HPKE . Sender (
128- recipientKey: self . pkR,
129- ciphersuite: self . ct,
205+ recipientKey: self . core . pkR,
206+ ciphersuite: self . core . ct,
130207 info: Data . oDoHQueryInfo
131208 )
132209
133210 let sealedData = try context. seal (
134211 queryPlain. encode ( ) ,
135- authenticating: self . aad ( . query, key: self . keyID)
212+ authenticating: self . core . aad ( . query, key: self . core . keyID)
136213 )
137214 let encapsulatedKey = context. encapsulatedKey
138215
@@ -146,7 +223,7 @@ public enum ODoH: Sendable {
146223
147224 let message = Message (
148225 messageType: . query,
149- keyID: self . keyID,
226+ keyID: self . core . keyID,
150227 encryptedMessage: encryptedMessage
151228 )
152229
@@ -178,20 +255,20 @@ public enum ODoH: Sendable {
178255 let responseEncrypted = response. encryptedMessage
179256
180257 // Derive secrets according to RFC
181- let ( aeadKey, aeadNonce) = try self . deriveSecrets (
258+ let ( aeadKey, aeadNonce) = try self . core . deriveSecrets (
182259 secret: context. exportSecret (
183260 context: Data . oDoHResponseInfo,
184- outputByteCount: self . ct. aead. keyByteCount
261+ outputByteCount: self . core . ct. aead. keyByteCount
185262 ) ,
186263 queryPlain: queryPlain. encode ( ) ,
187264 responseNonce: responseNonce
188265 )
189266
190267 // Build AAD for response
191- let aad = self . aad ( . response, key: responseNonce)
268+ let aad = self . core . aad ( . response, key: responseNonce)
192269
193270 // Decrypt using derived key/nonce (regular AEAD, not HPKE)
194- var plaintext = try self . ct. aead. open (
271+ var plaintext = try self . core . ct. aead. open (
195272 responseEncrypted,
196273 nonce: aeadNonce,
197274 authenticating: aad,
@@ -262,6 +339,25 @@ public enum ODoH: Sendable {
262339 response: response
263340 )
264341 }
342+ }
343+
344+ /// ODoH server routine for decrypting queries and encrypting responses.
345+ ///
346+ /// Handles the server-side cryptographic operations for Oblivious DNS over HTTPS.
347+ /// Initialized with a configuration containing the server's public key and algorithm parameters.
348+ public struct ServerRoutine {
349+ private var core : RoutineCore
350+
351+ /// Initialize ODoH server routine with server configuration.
352+ ///
353+ /// Extracts the HPKE ciphersuite parameters and derives the key identifier
354+ /// from the provided configuration. The key identifier is computed using
355+ /// HKDF as specified in RFC 9230 Section 6.2.
356+ ///
357+ /// - Parameter configuration: Server's ODoH configuration
358+ public init ( configuration: Configuration ) throws {
359+ self . core = try RoutineCore ( configuration: configuration)
360+ }
265361
266362 /// Decrypt DNS query using server's private key.
267363 ///
@@ -283,22 +379,22 @@ public enum ODoH: Sendable {
283379 }
284380
285381 var ciphertext = query. encryptedMessage
286- guard let enc = ciphertext. popFirst ( self . ct. kem. encapsulatedKeySize) else {
382+ guard let enc = ciphertext. popFirst ( self . core . ct. kem. encapsulatedKeySize) else {
287383 throw CryptoKitError . incorrectParameterSize
288384 }
289385
290386 // Setup HPKE recipient context
291387 var context = try HPKE . Recipient (
292388 privateKey: privateKey,
293- ciphersuite: self . ct,
389+ ciphersuite: self . core . ct,
294390 info: Data . oDoHQueryInfo,
295391 encapsulatedKey: enc
296392 )
297393
298394 // Decrypt query
299395 var plaintext = try context. open (
300396 ciphertext,
301- authenticating: self . aad ( . query, key: self . keyID)
397+ authenticating: self . core . aad ( . query, key: self . core . keyID)
302398 )
303399
304400 return QueryDecryptionResult ( plaintextQuery: try MessagePlaintext ( decoding: & plaintext) , context: context)
@@ -337,25 +433,25 @@ public enum ODoH: Sendable {
337433 responsePlain: MessagePlaintext
338434 ) throws -> Data {
339435 // Generate response nonce: random(max(Nn, Nk))
340- let nonceSize = self . ct. aead. nonceByteCount
341- let keySize = self . ct. aead. keyByteCount
436+ let nonceSize = self . core . ct. aead. nonceByteCount
437+ let keySize = self . core . ct. aead. keyByteCount
342438 let responseNonceSize = max ( nonceSize, keySize)
343439 let responseNonce = Data ( ( 0 ..< responseNonceSize) . map { _ in UInt8 . random ( in: 0 ... 255 ) } )
344440
345441 // Derive secrets
346- let ( aeadKey, aeadNonce) = try self . deriveSecrets (
442+ let ( aeadKey, aeadNonce) = try self . core . deriveSecrets (
347443 secret: recipient. exportSecret (
348444 context: Data . oDoHResponseInfo,
349- outputByteCount: self . ct. aead. keyByteCount
445+ outputByteCount: self . core . ct. aead. keyByteCount
350446 ) ,
351447 queryPlain: queryPlain. encode ( ) ,
352448 responseNonce: responseNonce
353449 )
354450
355451 // Encrypt response using derived keys (regular AEAD)
356- let encrypted = try self . ct. aead. seal (
452+ let encrypted = try self . core . ct. aead. seal (
357453 responsePlain. encode ( ) ,
358- authenticating: self . aad ( . response, key: responseNonce) ,
454+ authenticating: self . core . aad ( . response, key: responseNonce) ,
359455 nonce: aeadNonce,
360456 using: aeadKey
361457 )
@@ -392,64 +488,6 @@ public enum ODoH: Sendable {
392488 responsePlain: responsePlain
393489 )
394490 }
395-
396- /// Derive AEAD key and nonce for response encryption.
397- ///
398- /// Uses HKDF Extract-and-Expand with query plaintext and response nonce as salt.
399- /// Formula: Extract(Q_plain || len(nonce) || nonce, secret) → Expand for key/nonce.
400- ///
401- /// - Parameters:
402- /// - secret: Exported secret from HPKE context
403- /// - queryPlain: Original query plaintext
404- /// - responseNonce: Server-generated nonce
405- /// - Returns: Derived AEAD key and nonce
406- private func deriveSecrets(
407- secret: SymmetricKey ,
408- queryPlain: Data ,
409- responseNonce: Data
410- ) throws -> ( key: SymmetricKey , nonce: Data ) {
411- // Build salt: Q_plain || len(resp_nonce) || resp_nonce
412- var salt = Data ( )
413- salt. reserveCapacity ( queryPlain. count + 2 + responseNonce. count)
414- salt. append ( queryPlain)
415- salt. append ( bigEndianBytes: UInt16 ( responseNonce. count) )
416- salt. append ( responseNonce)
417-
418- // Extract PRK
419- let prk = self . ct. kdf. extract ( salt: salt, ikm: secret)
420-
421- // Expand to get key and nonce
422- let key = self . ct. kdf. expand (
423- prk: prk,
424- info: Data . oDoHKeyInfo,
425- outputByteCount: self . ct. aead. keyByteCount
426- )
427- let nonce = self . ct. kdf. expand (
428- prk: prk,
429- info: Data . oDoHNonceInfo,
430- outputByteCount: self . ct. aead. nonceByteCount
431- )
432-
433- return ( key, Data ( nonce) )
434- }
435-
436- /// Construct Additional Authenticated Data (AAD) for AEAD operations.
437- ///
438- /// Format: message_type (1 byte) || key_length (2 bytes) || key_data
439- ///
440- /// - Parameters:
441- /// - type: Message type (query or response)
442- /// - key: Key identifier or response nonce
443- /// - Returns: AAD bytes for AEAD operations
444- private func aad( _ type: Message . MessageType , key: Data ) -> Data {
445- var aad = Data ( )
446- let keyLength = UInt16 ( key. count)
447- aad. reserveCapacity ( 1 + 2 + key. count)
448- aad. append ( type. rawValue)
449- aad. append ( bigEndianBytes: keyLength)
450- aad. append ( key)
451- return aad
452- }
453491 }
454492
455493 // - MARK: Protocol Types
0 commit comments