mount public data from the atmosphere to a virtual filesystem (macos only) pdfs.at
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(atproto/oauth): add DPoPKey protocol + in-memory ES256 impl

+99
+54
Packages/ATProto/Sources/ATProto/OAuth/DPoP/DPoPKey.swift
··· 1 + import Foundation 2 + @preconcurrency import CryptoKit 3 + 4 + /// Abstraction over a DPoP signing key. v1 ships an in-memory ES256 impl 5 + /// via CryptoKit; a future impl can back the key with the Secure Enclave. 6 + public protocol DPoPKey: Sendable { 7 + /// The public JWK (JSON Web Key) for this key, as a JSON-compatible 8 + /// `[String: Any]` dict. Consumers embed this in the DPoP JWT header. 9 + func publicJWK() async throws -> [String: Any] 10 + 11 + /// Signs the given message with ES256, returning the raw r||s concatenation 12 + /// (64 bytes). Callers convert to JWS b64url as needed. 13 + func sign(_ message: Data) async throws -> Data 14 + 15 + /// Returns the underlying CryptoKit public key for verification in tests. 16 + func publicKey() async throws -> CryptoKit.P256.Signing.PublicKey 17 + } 18 + 19 + /// In-memory ES256 keypair for dev and tests. The host app will eventually 20 + /// provide a Secure Enclave-backed implementation under the same protocol. 21 + public final class InMemoryES256DPoPKey: DPoPKey, Sendable { 22 + private let privateKey: CryptoKit.P256.Signing.PrivateKey 23 + 24 + public static func generate() throws -> InMemoryES256DPoPKey { 25 + InMemoryES256DPoPKey(privateKey: CryptoKit.P256.Signing.PrivateKey()) 26 + } 27 + 28 + init(privateKey: CryptoKit.P256.Signing.PrivateKey) { 29 + self.privateKey = privateKey 30 + } 31 + 32 + public func publicJWK() async throws -> [String: Any] { 33 + let rep = privateKey.publicKey.x963Representation 34 + // x963 format: 0x04 || X (32 bytes) || Y (32 bytes) 35 + let x = rep.subdata(in: 1..<33) 36 + let y = rep.subdata(in: 33..<65) 37 + return [ 38 + "kty": "EC", 39 + "crv": "P-256", 40 + "alg": "ES256", 41 + "x": PKCE.base64URLEncode(x), 42 + "y": PKCE.base64URLEncode(y), 43 + ] 44 + } 45 + 46 + public func sign(_ message: Data) async throws -> Data { 47 + let signature = try privateKey.signature(for: message) 48 + return signature.rawRepresentation 49 + } 50 + 51 + public func publicKey() async throws -> CryptoKit.P256.Signing.PublicKey { 52 + privateKey.publicKey 53 + } 54 + }
+45
Packages/ATProto/Tests/ATProtoTests/OAuth/DPoPKeyTests.swift
··· 1 + import Foundation 2 + import CryptoKit 3 + import Testing 4 + @testable import ATProto 5 + 6 + @Suite("DPoPKey") 7 + struct DPoPKeyTests { 8 + @Test("ES256 key produces JWK with EC P-256 parameters") 9 + func jwk() async throws { 10 + let key = try InMemoryES256DPoPKey.generate() 11 + let jwk = try await key.publicJWK() 12 + #expect(jwk["kty"] as? String == "EC") 13 + #expect(jwk["crv"] as? String == "P-256") 14 + #expect(jwk["alg"] as? String == "ES256") 15 + // x and y are 32-byte coordinates, base64url-no-pad = 43 chars. 16 + let x = jwk["x"] as? String ?? "" 17 + let y = jwk["y"] as? String ?? "" 18 + #expect(x.count == 43) 19 + #expect(y.count == 43) 20 + } 21 + 22 + @Test("sign + verify round-trip with raw ECDSA P-256") 23 + func signVerify() async throws { 24 + let key = try InMemoryES256DPoPKey.generate() 25 + let message = Data("hello dpop".utf8) 26 + let signature = try await key.sign(message) 27 + 28 + // Signature is 64 bytes: r||s, each 32 bytes. 29 + #expect(signature.count == 64) 30 + 31 + // Verify using CryptoKit directly. 32 + let publicKey = try await key.publicKey() 33 + let ckSig = try CryptoKit.P256.Signing.ECDSASignature(rawRepresentation: signature) 34 + #expect(publicKey.isValidSignature(ckSig, for: message)) 35 + } 36 + 37 + @Test("two generate() calls produce distinct keys") 38 + func uniqueKeys() async throws { 39 + let a = try InMemoryES256DPoPKey.generate() 40 + let b = try InMemoryES256DPoPKey.generate() 41 + let jwkA = try await a.publicJWK() 42 + let jwkB = try await b.publicJWK() 43 + #expect(jwkA["x"] as? String != jwkB["x"] as? String) 44 + } 45 + }