Skip to content

Commit 99863f6

Browse files
committed
add support for Login provider
1 parent b4371f0 commit 99863f6

13 files changed

Lines changed: 1941 additions & 2 deletions

Sources/SotoCore/Credential/CredentialProvider.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,13 @@ extension CredentialProviderFactory {
7070
/// files. On macOS is looks in the environment and then the config file.
7171
public static var `default`: CredentialProviderFactory {
7272
#if os(Linux)
73-
return .selector(.environment, .ecs, .ec2, .configFile())
73+
return .selector(.environment, .ecs, .ec2, .configFile(), .login())
7474
#else
75-
return .selector(.environment, .configFile())
75+
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) {
76+
return .selector(.environment, .configFile(), .login())
77+
} else {
78+
return .selector(.environment, .configFile())
79+
}
7680
#endif
7781
}
7882

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Soto for AWS open source project
4+
//
5+
// Copyright (c) 2017-2023 the Soto project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Soto project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
// DPoP Token Generator - JWT with EC Signing
16+
17+
import Crypto
18+
import Foundation
19+
20+
struct DPoPTokenGenerator {
21+
22+
// allow to inject these two values during the tests
23+
private let jtiGenerator: @Sendable () -> String
24+
private let timeProvider: @Sendable () -> Int
25+
26+
init(
27+
jtiGenerator: @escaping @Sendable () -> String = { UUID().uuidString },
28+
timeProvider: @escaping @Sendable () -> Int = { Int(Date().timeIntervalSince1970) }
29+
) {
30+
self.jtiGenerator = jtiGenerator
31+
self.timeProvider = timeProvider
32+
}
33+
34+
private struct JWK: Codable {
35+
let kty: String
36+
let crv: String
37+
let x: String
38+
let y: String
39+
}
40+
41+
private struct JWTHeader: Codable {
42+
let typ: String
43+
let alg: String
44+
let jwk: JWK
45+
}
46+
47+
private struct JWTPayload: Codable {
48+
let jti: String
49+
let htm: String
50+
let htu: String
51+
let iat: Int
52+
}
53+
54+
private func base64URLEncode(_ data: Data) -> String {
55+
data.base64EncodedString()
56+
.replacingOccurrences(of: "+", with: "-")
57+
.replacingOccurrences(of: "/", with: "_")
58+
.replacingOccurrences(of: "=", with: "")
59+
}
60+
61+
@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *)
62+
private func extractPublicKeyCoordinates(from pemKey: String) -> (x: String, y: String)? {
63+
// Use swift-crypto's built-in PEM support to load the key
64+
guard let privateKey = try? P256.Signing.PrivateKey(pemRepresentation: pemKey) else {
65+
return nil
66+
}
67+
68+
// Get the public key and extract x963 representation
69+
let publicKey = privateKey.publicKey
70+
let x963Data = publicKey.x963Representation
71+
72+
// X963 format for uncompressed point: 0x04 + x (32 bytes) + y (32 bytes)
73+
guard x963Data.count == 65, x963Data[0] == 0x04 else {
74+
return nil
75+
}
76+
77+
let xData = x963Data[1..<33]
78+
let yData = x963Data[33..<65]
79+
80+
return (
81+
x: base64URLEncode(xData),
82+
y: base64URLEncode(yData)
83+
)
84+
}
85+
86+
@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *)
87+
private func signWithEC(message: Data, pemKey: String) throws -> Data {
88+
// Use swift-crypto's built-in PEM support
89+
let privateKey = try P256.Signing.PrivateKey(pemRepresentation: pemKey)
90+
91+
// Sign the message (Crypto automatically hashes with SHA-256 for ES256)
92+
let signature = try privateKey.signature(for: message)
93+
94+
// Return raw signature (r + s concatenated, 64 bytes for P-256)
95+
return signature.rawRepresentation
96+
}
97+
98+
@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *)
99+
func generateDPoPHeader(
100+
endpoint: String,
101+
httpMethod: String = "POST",
102+
pemKey: String
103+
) throws -> String {
104+
let encoder = JSONEncoder()
105+
encoder.outputFormatting = .sortedKeys
106+
107+
// Extract public key coordinates from PEM
108+
guard let (x, y) = extractPublicKeyCoordinates(from: pemKey) else {
109+
throw LoginError.tokenParseFailed
110+
}
111+
112+
// JWT header
113+
let header = JWTHeader(
114+
typ: "dpop+jwt",
115+
alg: "ES256",
116+
jwk: JWK(kty: "EC", crv: "P-256", x: x, y: y)
117+
)
118+
119+
// JWT payload
120+
let payload = JWTPayload(
121+
jti: jtiGenerator(),
122+
htm: httpMethod,
123+
htu: endpoint,
124+
iat: timeProvider()
125+
)
126+
127+
let headerData = try encoder.encode(header)
128+
let payloadData = try encoder.encode(payload)
129+
130+
let headerB64 = base64URLEncode(headerData)
131+
let payloadB64 = base64URLEncode(payloadData)
132+
133+
let message = "\(headerB64).\(payloadB64)"
134+
let messageData = message.data(using: .utf8)!
135+
136+
// Sign the message with EC private key
137+
let signatureData = try signWithEC(message: messageData, pemKey: pemKey)
138+
let signatureB64 = base64URLEncode(signatureData)
139+
140+
return "\(message).\(signatureB64)"
141+
}
142+
}

0 commit comments

Comments
 (0)