Skip to content

Commit

Permalink
Fanout (#19)
Browse files Browse the repository at this point in the history
* Start fanout implementation

* Add ack

* Parse fanout message

* Update fanout message parsing

* FanoutClient for publishing messages

* Add fanout public key

* Unsafe fanout signature verification

* Fix signature length check

* Verify fanout expiration date

* Support meta keys

* Add connection id

* Refactor upgrade syntax

* Fix value signature

* Mark discardable

* Stub fanout token tests

* Add algo stubs

* Stub signatures

* Stub fanout verify

* Indent

* Verify ecdsa signatures

* Deps

* Deps

* Update ECDSA api

* Deps

* Update demo

* Add justfile

* Deps

* Add back unsafe verify

* Use swift-crypto

* Deps

* Cleanup crypto

* Cleanup fanout verification

* Fix uppercase

* Fix bytes
  • Loading branch information
AndrewBarba authored Feb 8, 2023
1 parent 21b00fe commit 4b9b48e
Show file tree
Hide file tree
Showing 16 changed files with 615 additions and 65 deletions.
5 changes: 5 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
build:
swift build -c debug --triple wasm32-unknown-wasi

demo: build
fastly compute serve --skip-build --file ./.build/debug/ComputeDemo.wasm
8 changes: 4 additions & 4 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"pins" : [
{
"identity" : "crypto",
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-cloud/Crypto",
"location" : "https://github.com/swift-cloud/swift-crypto",
"state" : {
"revision" : "defff250bbd79d81fb8ee53926360fc522d75bad",
"version" : "1.6.0"
"revision" : "dd30eb5aaeea62282ef2ab32c9f6e1976145f920",
"version" : "2.2.4"
}
}
],
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ let package = Package(
.library(name: "Compute", targets: ["Compute"])
],
dependencies: [
.package(url: "https://github.com/swift-cloud/Crypto", from: "1.6.0")
.package(url: "https://github.com/swift-cloud/swift-crypto", "1.0.0" ..< "3.0.0")
],
targets: [
.target(name: "Compute", dependencies: ["ComputeRuntime", "Crypto"]),
.target(name: "Compute", dependencies: ["ComputeRuntime", .product(name: "Crypto", package: "swift-crypto")]),
.target(name: "ComputeRuntime"),
.executableTarget(name: "ComputeDemo", dependencies: ["Compute"]),
.testTarget(name: "ComputeTests", dependencies: ["Compute"])
Expand Down
190 changes: 190 additions & 0 deletions Sources/Compute/Crypto.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
//
// Crypto.swift
//
//
// Created by Andrew Barba on 2/8/23.
//

import Crypto

public enum Crypto {}

// MARK: - Hashing

extension Crypto {

public static func hash<T>(_ input: String, using hash: T.Type) -> T.Digest where T: HashFunction {
return T.hash(data: Data(input.utf8))
}

public static func hash<T>(_ input: [UInt8], using hash: T.Type) -> T.Digest where T: HashFunction {
return T.hash(data: Data(input))
}

public static func hash<T>(_ input: Data, using hash: T.Type) -> T.Digest where T: HashFunction {
return T.hash(data: input)
}

public static func sha256(_ input: String) -> SHA256.Digest {
return hash(input, using: SHA256.self)
}

public static func sha256(_ input: [UInt8]) -> SHA256.Digest {
return hash(input, using: SHA256.self)
}

public static func sha256(_ input: Data) -> SHA256.Digest {
return hash(input, using: SHA256.self)
}

public static func sha384(_ input: String) -> SHA384.Digest {
return hash(input, using: SHA384.self)
}

public static func sha384(_ input: [UInt8]) -> SHA384.Digest {
return hash(input, using: SHA384.self)
}

public static func sha384(_ input: Data) -> SHA384.Digest {
return hash(input, using: SHA384.self)
}

public static func sha512(_ input: String) -> SHA512.Digest {
return hash(input, using: SHA512.self)
}

public static func sha512(_ input: [UInt8]) -> SHA512.Digest {
return hash(input, using: SHA512.self)
}

public static func sha512(_ input: Data) -> SHA512.Digest {
return hash(input, using: SHA512.self)
}
}

// MARK: - HMAC

extension Crypto {
public enum Auth {
public enum Hash {
case sha256
case sha384
case sha512
}

public static func code(for input: String, secret: String, using hash: Hash) -> Data {
let data = Data(input.utf8)
let key = SymmetricKey(data: Data(secret.utf8))
switch hash {
case .sha256:
return HMAC<SHA256>.authenticationCode(for: data, using: key).data
case .sha384:
return HMAC<SHA384>.authenticationCode(for: data, using: key).data
case .sha512:
return HMAC<SHA512>.authenticationCode(for: data, using: key).data
}
}

public static func verify(_ input: String, signature: Data, secret: String, using hash: Hash) -> Bool {
let computed = code(for: input, secret: secret, using: hash)
return computed.toHexString() == signature.toHexString()
}
}
}

// MARK: - ECDSA

extension Crypto {
public enum ECDSA {
public enum Algorithm {
case p256
case p384
case p521
}

public static func signature(for input: String, secret: String, using algorithm: Algorithm) throws -> Data {
switch algorithm {
case .p256:
let pk = try P256.Signing.PrivateKey(pemRepresentation: secret)
return try pk.signature(for: Crypto.sha256(input)).rawRepresentation
case .p384:
let pk = try P384.Signing.PrivateKey(pemRepresentation: secret)
return try pk.signature(for: Crypto.sha384(input)).rawRepresentation
case .p521:
let pk = try P521.Signing.PrivateKey(pemRepresentation: secret)
return try pk.signature(for: Crypto.sha512(input)).rawRepresentation
}
}

public static func verify(_ input: String, signature: Data, key: String, using algorithm: Algorithm) throws -> Bool {
switch algorithm {
case .p256:
let publicKey = try P256.Signing.PublicKey(pemRepresentation: key)
let ecdsaSignature = try P256.Signing.ECDSASignature(rawRepresentation: signature)
return publicKey.isValidSignature(ecdsaSignature, for: Crypto.sha256(input))
case .p384:
let publicKey = try P384.Signing.PublicKey(pemRepresentation: key)
let ecdsaSignature = try P384.Signing.ECDSASignature(rawRepresentation: signature)
return publicKey.isValidSignature(ecdsaSignature, for: Crypto.sha384(input))
case .p521:
let publicKey = try P521.Signing.PublicKey(pemRepresentation: key)
let ecdsaSignature = try P521.Signing.ECDSASignature(rawRepresentation: signature)
return publicKey.isValidSignature(ecdsaSignature, for: Crypto.sha512(input))
}
}
}
}

// MARK: - Utils

extension DataProtocol {
public var bytes: [UInt8] {
return .init(self)
}

public var data: Data {
return .init(self)
}

public func toHexString() -> String {
return reduce("") {$0 + String(format: "%02x", $1)}
}
}

extension Digest {
public var bytes: [UInt8] {
return .init(self)
}

public var data: Data {
return .init(self)
}

public func toHexString() -> String {
return reduce("") {$0 + String(format: "%02x", $1)}
}
}

extension HashedAuthenticationCode {
public var bytes: [UInt8] {
return .init(self)
}

public var data: Data {
return .init(self)
}

public func toHexString() -> String {
return reduce("") {$0 + String(format: "%02x", $1)}
}
}

extension Array where Element == UInt8 {
public var data: Data {
return .init(self)
}

public func toHexString() -> String {
return reduce("") {$0 + String(format: "%02x", $1)}
}
}
110 changes: 110 additions & 0 deletions Sources/Compute/Fanout/FanoutClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// FanoutClient.swift
//
//
// Created by Andrew Barba on 2/1/23.
//

import Foundation

public struct FanoutClient: Sendable {

public let service: String

public let hostname: String

private let token: String

private var publishEndpoint: String {
"https://\(hostname)/service/\(service)/publish/"
}

public init(service: String = Fastly.Environment.serviceId, token: String, hostname: String = "api.fastly.com") {
self.service = service
self.token = token
self.hostname = hostname
}

@discardableResult
public func publish(_ message: PublishMessage) async throws -> FetchResponse {
return try await fetch(publishEndpoint, .options(
method: .post,
body: .json(message),
headers: ["Fastly-Key": token]
))
}

@discardableResult
public func publish(_ content: String, to channel: String) async throws -> FetchResponse {
let message = PublishMessage(items: [
.init(channel: channel, formats: .init(wsMessage: .init(content: content)))
])
return try await publish(message)
}

@discardableResult
public func publish<T: Encodable>(_ value: T, encoder: JSONEncoder = .init(), to channel: String) async throws -> FetchResponse {
let content = try encoder.encode(value)
return try await publish(content, to: channel)
}

@discardableResult
public func publish(_ json: Any, to channel: String) async throws -> FetchResponse {
let data = try JSONSerialization.data(withJSONObject: json)
let content = String(data: data, encoding: .utf8)
return try await publish(content, to: channel)
}

@discardableResult
public func publish(_ jsonObject: [String: Any], to channel: String) async throws -> FetchResponse {
let data = try JSONSerialization.data(withJSONObject: jsonObject)
let content = String(data: data, encoding: .utf8)
return try await publish(content, to: channel)
}

@discardableResult
public func publish(_ jsonArray: [Any], to channel: String) async throws -> FetchResponse {
let data = try JSONSerialization.data(withJSONObject: jsonArray)
let content = String(data: data, encoding: .utf8)
return try await publish(content, to: channel)
}
}

extension FanoutClient {
public struct PublishMessage: Codable, Sendable {
public let items: [PublishMessageItem]

public init(items: [PublishMessageItem]) {
self.items = items
}
}

public struct PublishMessageItem: Codable, Sendable {
public let channel: String
public let formats: PublishMessageItemFormats

public init(channel: String, formats: PublishMessageItemFormats) {
self.channel = channel
self.formats = formats
}
}

public struct PublishMessageItemFormats: Codable, Sendable {
enum CodingKeys: String, CodingKey {
case wsMessage = "ws-message"
}
public let wsMessage: PublishMessageItemContent

public init(wsMessage: PublishMessageItemContent) {
self.wsMessage = wsMessage
}
}

public struct PublishMessageItemContent: Codable, Sendable {
public let content: String

public init(content: String) {
self.content = content
}
}
}
Loading

0 comments on commit 4b9b48e

Please sign in to comment.