Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
584c70e
feat(mcp): support per-server TLS trust for remote MCP servers
karup Oct 28, 2026
540b0ec
Merge branch 'dev' into mcp-tls-config
karup Oct 28, 2026
ece89a5
Merge branch 'dev' into mcp-tls-config
karup Oct 28, 2026
70e84f0
Merge branch 'dev' into mcp-tls-config
karup Oct 28, 2026
414b684
Merge branch 'dev' into mcp-tls-config
karup Oct 29, 2026
16d8f54
Merge branch 'dev' into mcp-tls-config
karup Oct 29, 2026
2c27162
Merge branch 'dev' into mcp-tls-config
karup Oct 29, 2026
11286ee
Merge branch 'dev' into mcp-tls-config
karup Oct 29, 2026
0b3f02a
Merge branch 'dev' into mcp-tls-config
karup Oct 29, 2026
aa72ed2
Merge branch 'dev' into mcp-tls-config
karup Oct 29, 2026
0218a86
Merge branch 'dev' into mcp-tls-config
karup Oct 29, 2026
8972042
fix(core): skip name matching for fingerprint-pinned TLS certificates
karup Oct 29, 2026
0b8837c
docs: note that fingerprint pins skip name matching
karup Oct 29, 2026
f25ece6
Merge branch 'dev' into mcp-tls-config
karup Oct 29, 2026
120c438
fix(core): re-verify pinned certificates on every handshake
karup Oct 29, 2026
6e017a5
Merge branch 'dev' into mcp-tls-config
karup Oct 29, 2026
6ca159d
Merge branch 'dev' into mcp-tls-config
karup Oct 29, 2026
a37bd9e
Merge branch 'dev' into mcp-tls-config
karup Oct 29, 2026
4ee5a94
Merge branch 'dev' into mcp-tls-config
karup Oct 29, 2026
bea71d9
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
b1ed8e9
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
134d0bf
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
3c8d39e
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
06a804f
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
0935321
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
3590ba4
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
4f5510c
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
221f0b9
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
7fdb314
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
6c27587
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
8f5a2a8
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
1d5111b
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
12f8d5c
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
a45fc8c
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
45ba66b
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
2250fc4
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
560d45b
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
3c9f8c4
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
c2e77ce
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
d8e3bac
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
045b8ab
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
e218290
Merge branch 'dev' into mcp-tls-config
karup Oct 30, 2026
380b49a
Merge branch 'dev' into mcp-tls-config
karup Oct 31, 2026
42b08e8
Merge branch 'dev' into mcp-tls-config
karup Oct 31, 2026
d6639b6
Merge branch 'dev' into mcp-tls-config
karup Oct 31, 2026
15f739f
Merge branch 'dev' into mcp-tls-config
karup Oct 31, 2026
360d0eb
Merge branch 'dev' into mcp-tls-config
karup Oct 31, 2026
33677d0
Merge branch 'dev' into mcp-tls-config
karup Nov 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/core/src/mcp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,16 +302,22 @@ export const connect = Effect.fnUntraced(function* (
return yield* new ConnectError({ server, message: `Invalid MCP URL for "${server}"` })
const { McpOAuth } = yield* Effect.promise(() => import("./oauth.js"))
const fetch = yield* McpOAuth.loggedFetch({ server, directory })
const { McpTls } = yield* Effect.promise(() => import("./tls.js"))
// Servers that bundle their own Code Mode (Cloudflare and others) expose raw tools when asked
// with ?codemode=false, which is what our Code Mode wants. The configured URL stays the OAuth identity.
const url = new URL(config.url)
const addedCodemode = config.codemode !== false && !url.searchParams.has("codemode") && !rejectsUnknownQuery(url)
if (addedCodemode) url.searchParams.set("codemode", "false")
// A missing CA file or a fingerprint mismatch fails the connect instead of the first request.
const tls = config.tls
const resolved = tls
? yield* Effect.tryPromise({ try: () => McpTls.resolveTls(tls, directory, url), catch: toError })
: undefined
const open = Effect.fnUntraced(function* (url: URL) {
session.transport = new StreamableHTTPClientTransport(url, {
requestInit: config.headers ? { headers: config.headers } : undefined,
authProvider,
fetch,
fetch: resolved ? McpTls.withTls(resolved, fetch) : fetch,
})
return yield* initialize(session.transport)
})
Expand Down
246 changes: 246 additions & 0 deletions packages/core/src/mcp/tls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
export * as McpTls from "./tls.js"

import { createHash } from "node:crypto"
import { closeSync, fstatSync, openSync, readFileSync } from "node:fs"
import { isIP } from "node:net"
import { homedir } from "node:os"
import path from "node:path"
import tls, { type PeerCertificate } from "node:tls"
import type { ConfigMCP } from "@opencode/schema/config/mcp"

const PEM_CERT_HEADER = "-----BEGIN CERTIFICATE-----"
const PEM_CERT_FOOTER = "-----END CERTIFICATE-----"
const MAX_CA_FILE_SIZE = 1024 * 1024
const FINGERPRINT_PATTERN = /^(?:SHA256:)?(?:[0-9A-Fa-f]{2}:){31}[0-9A-Fa-f]{2}$|^(?:SHA256:)?[0-9A-Fa-f]{64}$/

/**
* Resolve a CA file path: `~` expands to the home directory, absolute paths pass through,
* and relative paths resolve from the workspace directory.
*/
export function resolveFilePath(filePath: string, workspaceDir: string): string {
if (filePath.startsWith("~")) return path.join(homedir(), filePath.slice(2))
if (path.isAbsolute(filePath)) return filePath
return path.resolve(workspaceDir, filePath)
}

/**
* Validate that content holds at least one well-formed PEM certificate block. Cryptographic
* validation is the TLS layer's job at connection time; certificates may be concatenated, so
* only the first BEGIN/END pair must be complete.
*/
export function validatePemCert(content: string, source: string): void {
const beginIdx = content.indexOf(PEM_CERT_HEADER)
if (beginIdx === -1) throw new Error(`${source}: missing PEM certificate header ("${PEM_CERT_HEADER}")`)
const endIdx = content.indexOf(PEM_CERT_FOOTER, beginIdx)
if (endIdx === -1) throw new Error(`${source}: missing PEM certificate footer ("${PEM_CERT_FOOTER}")`)
if (content.slice(beginIdx + PEM_CERT_HEADER.length, endIdx).trim().length === 0)
throw new Error(`${source}: empty PEM certificate body`)
}

/**
* Validate a SHA256 fingerprint and normalize it to lowercase hex without prefix or colons.
*/
export function validateFingerprint(raw: string): string {
const trimmed = raw.trim()
if (!FINGERPRINT_PATTERN.test(trimmed))
throw new Error(
`Invalid SHA256 fingerprint: "${trimmed}". Expected 64 hex characters, ` +
`optionally prefixed with "SHA256:" and separated by colons (e.g. "SHA256:AA:BB:CC:DD:...").`,
)
return trimmed.replace(/^SHA256:/i, "").replace(/:/g, "").toLowerCase()
}

/**
* Read and validate a CA certificate file. The file is opened once and the descriptor is used
* for both stat and read, so it cannot change between validation and content retrieval.
*/
export function readCaFile(filePath: string, workspaceDir: string): string {
const resolved = resolveFilePath(filePath, workspaceDir)
let fd: number
try {
fd = openSync(resolved, "r")
} catch {
throw new Error(`CA file not found: ${filePath}`)
}
try {
const stat = fstatSync(fd)
if (!stat.isFile()) throw new Error(`CA path is not a file: ${filePath}`)
if (stat.size > MAX_CA_FILE_SIZE)
throw new Error(`CA file too large (${stat.size} bytes, max ${MAX_CA_FILE_SIZE}): ${filePath}`)
const content = readFileSync(fd, "utf-8").trim()
validatePemCert(content, filePath)
return content
} finally {
closeSync(fd)
}
}

/**
* The trust rules a server's connections are subject to. `ca` is a PEM trust anchor bundle;
* `fingerprint` pins one exact certificate, whose SHA256 fingerprint is re-checked on every
* handshake.
*/
export type ResolvedTls = {
readonly ca: string
readonly fingerprint?: string
}

/**
* Resolve a server's TLS config into the trust rules for its connections. An undefined result
* means "use the system trust store". A pinned fingerprint opens one pre-flight TLS connection;
* the presented certificate must match it exactly and becomes the trust anchor for the real
* connection, so chain verification only accepts that certificate. Name matching is deliberately
* not enforced: LAN appliance certificates often omit the IP SANs a hostname check would require,
* and the fingerprint already identifies the certificate the user pinned.
*/
export async function resolveTls(
config: ConfigMCP.Tls,
workspaceDir: string,
url: URL,
): Promise<ResolvedTls | undefined> {
if (config.fingerprint !== undefined) {
const expected = validateFingerprint(config.fingerprint)
const cert = await probeCertificate(url.hostname, port(url))
if (cert.fingerprint !== expected)
throw new Error(`Certificate fingerprint mismatch.\nExpected: ${expected}\nGot: ${cert.fingerprint}`)
return { ca: cert.pem, fingerprint: expected }
}
if (config.ca_pem !== undefined) validatePemCert(config.ca_pem, "ca_pem config entry")
const ca = [
config.ca_pem,
config.ca_file !== undefined ? readCaFile(config.ca_file, workspaceDir) : undefined,
]
.filter((part): part is string => part !== undefined)
.join("")
return ca === "" ? undefined : { ca }
}

/**
* Wrap a fetch so its requests apply the resolved TLS trust rules. The trust is scoped to the
* returned fetch and does not affect other connections, including redirects it follows. A pinned
* fingerprint anchors chain verification to the pinned certificate itself, and the identity hook
* replaces the hostname check while re-checking the fingerprint on every handshake.
*/
export function withTls(
resolved: ResolvedTls,
base: (url: string | URL, init?: RequestInit) => Promise<Response>,
): (url: string | URL, init?: RequestInit) => Promise<Response> {
const fingerprint = resolved.fingerprint
if (fingerprint !== undefined) {
return (url, init) =>
base(url, {
...init,
tls: {
ca: resolved.ca,
// Replaces the hostname check: returning undefined accepts the certificate (even one
// without an IP SAN), returning an Error rejects it.
checkServerIdentity: (_hostname: string, cert: PeerCertificate) => {
const presented = createHash("sha256").update(cert.raw).digest("hex")
if (presented !== fingerprint)
return new Error(`Certificate fingerprint mismatch.\nExpected: ${fingerprint}\nGot: ${presented}`)
},
},
} as BunFetchRequestInit)
}
return (url, init) => base(url, { ...init, tls: { ca: resolved.ca } } as BunFetchRequestInit)
}

/**
* True when a connection failure is a TLS certificate verification error rather than a network,
* protocol, or HTTP failure. Current runtimes report these as message-only errors ("self signed
* certificate", "ERR_TLS_CERT_ALTNAME_INVALID", "certificate has expired"); Node-style codes are
* matched as well for the runtimes that expose them.
*/
export function isTlsCertificateError(error: unknown): boolean {
Comment thread
karup marked this conversation as resolved.
Resolved
const patterns = [
/self[- ]signed/i,
/ERR_TLS_CERT_ALTNAME_INVALID|CERT_ALTNAME/i,
/UNABLE_TO_VERIFY_LEAF_SIGNATURE|UNABLE_TO_GET_ISSUER_CERT_LOCALLY|unable to verify/i,
/CERT_HAS_EXPIRED|certificate has expired/i,
/DEPTH_ZERO/i,
]
let current: unknown = error
for (let depth = 0; depth < 5; depth++) {
if (!(current instanceof Error)) break
const code = (current as { code?: unknown }).code
const text = `${code === undefined ? "" : `${code} `}${current.message}`
if (patterns.some((pattern) => pattern.test(text))) return true
current = (current as { cause?: unknown }).cause
}
return false
}

export interface Certificate {
/** The presented certificate as PEM, usable as a trust anchor. */
readonly pem: string
/** SHA256 fingerprint, lowercase hex without colons. */
readonly fingerprint: string
/** Distinguished name of the certificate subject, e.g. "CN=OPNsense, O=Foo Corp". */
readonly subject: string
/** Expiry as epoch milliseconds. */
readonly validTo: number
}

/**
* Read the certificate a server presents without validating it against any trust store. The
* socket is destroyed before any application data is sent.
*/
export function probeCertificate(hostname: string, portNumber: number): Promise<Certificate> {
return new Promise<Certificate>((resolve, reject) => {
// SNI rejects IP literals as server names, but LAN appliances are exactly the servers this
// feature targets, so omit the server name for IP addresses.
const bare = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname
const socket = tls.connect({
host: hostname,
port: portNumber,
rejectUnauthorized: false,
...(isIP(bare) === 0 ? { servername: hostname } : {}),
})
const fail = (message: string) => {
socket.destroy()
reject(new Error(message))
}
socket.once("secureConnect", () => {
const cert = socket.getPeerX509Certificate()
if (!cert) return fail("No certificate presented by server")
const presented = cert.fingerprint256
if (!presented) return fail("Could not compute certificate fingerprint")
const validTo = new Date(cert.validTo).getTime()
if (Number.isNaN(validTo)) return fail("Could not parse certificate validity")
socket.destroy()
resolve({
pem: derToPem(cert.raw),
fingerprint: presented.replace(/:/g, "").toLowerCase(),
subject: cert.subject,
validTo,
})
})
socket.once("error", (err: Error) => {
socket.destroy()
reject(new Error(`TLS pre-flight connection failed: ${err.message}`))
})
socket.setTimeout(10_000, () => {
socket.destroy()
reject(new Error("TLS pre-flight connection timed out"))
})
})
}

/**
* The port of a server URL, with the scheme default when none is stated.
*/
export function port(url: URL): number {
return Number(url.port) || (url.protocol === "https:" ? 443 : 80)
}

/**
* Convert a DER certificate to PEM so it can be used as a trust anchor. Bytes are base64-encoded
* in chunks to keep the spread argument small on large certificates.
*/
export function derToPem(der: ArrayBuffer): string {
const bytes = new Uint8Array(der)
const chunks: string[] = []
for (let i = 0; i < bytes.length; i += 4096) chunks.push(btoa(String.fromCharCode(...bytes.subarray(i, i + 4096))))
const lines = chunks.join("").match(/.{1,64}/g) ?? []
return `-----BEGIN CERTIFICATE-----\n${lines.join("\n")}\n-----END CERTIFICATE-----\n`
}
41 changes: 41 additions & 0 deletions packages/core/test/fixture/tls-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { readFileSync } from "node:fs"
import path from "node:path"
import { Server, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server"

export const TLS_FIXTURE_DIR = path.join(import.meta.dir, "tls")

// Long-lived self-signed certificate for 127.0.0.1 and localhost.
export const CERT = readFileSync(path.join(TLS_FIXTURE_DIR, "localhost.crt"), "utf-8")
export const KEY = readFileSync(path.join(TLS_FIXTURE_DIR, "localhost.key"), "utf-8")
// Appliance-style certificate: a CN and no subjectAltName. A hostname or IP name check fails
// against it, which is exactly why the fingerprint is the identity check when it is pinned.
export const CERT_APPLIANCE = readFileSync(path.join(TLS_FIXTURE_DIR, "appliance.crt"), "utf-8")
export const KEY_APPLIANCE = readFileSync(path.join(TLS_FIXTURE_DIR, "appliance.key"), "utf-8")

export function startTlsMcpServer(options: { cert?: string; key?: string; port?: number } = {}) {
const cert = options.cert ?? CERT
const key = options.key ?? KEY
const protocol = new Server({ name: "tls-test", version: "1.0.0" }, { capabilities: { tools: {} } })
protocol.setRequestHandler("tools/list", () =>
Promise.resolve({ tools: [{ name: "echo", inputSchema: { type: "object", properties: {} } }] }),
)
const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() })
return protocol.connect(transport).then(() => {
const http = Bun.serve({
port: options.port ?? 0,
hostname: "127.0.0.1",
tls: { cert, key },
fetch: (request) => transport.handleRequest(request),
})
const port = http.port
if (port === undefined) throw new Error("TLS server did not bind a port")
return {
url: `${http.url}/mcp`,
port,
close: async () => {
await protocol.close().catch(() => {})
http.stop(true)
},
}
})
}
19 changes: 19 additions & 0 deletions packages/core/test/fixture/tls/appliance.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDFTCCAf2gAwIBAgIUHT9Q3Me6KRf/symqQZXz7N440RowDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPYXBwbGlhbmNlLmxvY2FsMB4XDTI2MTAyOTIwMDAyMFoX
DTM2MTAyNjIwMDAyMFowGjEYMBYGA1UEAwwPYXBwbGlhbmNlLmxvY2FsMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAys+pDvBb235QEzENdwA0lWeeGVL7
oJOv3A9lC1m172GZp0xHdvlyke/CwptopRQjr5+3aUq7sIDw4phSSRUfCtTFkbC2
/AQ8ztnXhVcERuHz+uNZfUUnEpQUQiNmUDxACkuijAvCBlub/wj/VN2LJr2OeW3V
3D+KfW+f4ZtHCXPuDRGVBLqd21PgQJTRUTjXp+tUEp/B3Wr4bd2TPqzpIxJGM9/l
UOybgDFbz6k+3taOPQn9y5IcXU+8GD/AFbt6kxX7AXGvDGPe9hxop5uZxEKJhfQ5
XPPLlx1oXxbyGZxelwwUkjLgiXJW3ekFjYIOaIcEbe+nlkf+9+JnTxG3mwIDAQAB
o1MwUTAdBgNVHQ4EFgQUZE/eBykoTexgw0u6lIj4mka7+OowHwYDVR0jBBgwFoAU
ZE/eBykoTexgw0u6lIj4mka7+OowDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
AQsFAAOCAQEAFcV0CZbU7nt3tJ/sSn4p4X71v5gKBPUASk/Y/RFhZ2xnwAk4Cgs2
RCOlzCObF4+ZHYYVBaavTOtb4DWzd5jBeFQdzbYM+YWoQoujlDDxG+aerNMiMhnV
oD9J1vQSgRhHcYI2KJdxAi1cQK0JsA+xZ82Z6vup5KGLgUoZWd1pZKUdG1Mnlie7
1LPDCtVMDZvGFiJJdqI+GFiYahQ1w5R1uuuPuFujHiNK5QLQINq+K9eyDq2HSacU
mKFSKrTfa5z+rYye7E37JBJ62l1G7q0kGS90e4tQJCckAl7Om5fLdZnqlw/TcjNA
sA1yTeWDYZ7UCwD+NzZml13aRc5H70brxw==
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions packages/core/test/fixture/tls/appliance.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDKz6kO8FvbflAT
MQ13ADSVZ54ZUvugk6/cD2ULWbXvYZmnTEd2+XKR78LCm2ilFCOvn7dpSruwgPDi
mFJJFR8K1MWRsLb8BDzO2deFVwRG4fP641l9RScSlBRCI2ZQPEAKS6KMC8IGW5v/
CP9U3YsmvY55bdXcP4p9b5/hm0cJc+4NEZUEup3bU+BAlNFRONen61QSn8Hdavht
3ZM+rOkjEkYz3+VQ7JuAMVvPqT7e1o49Cf3LkhxdT7wYP8AVu3qTFfsBca8MY972
HGinm5nEQomF9Dlc88uXHWhfFvIZnF6XDBSSMuCJclbd6QWNgg5ohwRt76eWR/73
4mdPEbebAgMBAAECggEAGUzJcq8iTmHV/Gbonpk8dgzciJ00/aWe0Q1slftlekzL
HbcizSE2HSVzPtKjPcCi6QQ8HZrwigazxhipXyuO2UuMr+2DDjI7IbTEMYKW6n7C
cGBPKBTp2GP3mP6Qg17YwFAKIX1QAXoJHkoRjBUtCDKqZNSCUrzselRj9gFoXbHK
5jtjTVEZSs3smTM3crlRizdKNvzP+cqx4pac8kPjuDH9yT8oicg+Bnmt4/GAZsWp
brSYai7HScudx0A+IDLuKfP9tYVeIoe4ebgpyxamWDjrwVRpn5tDgE84A2knwVlN
kMSQ8ySleziCRZm1Z3GRdMv0W9zhI/6TmPXsctjtgQKBgQD+Mz5P72usOFdvEUem
sCid8SeKxeu/YM0ODbEOFqQRNf732ZJ05Q6NeJHmbA19DULCSYYgNiA+yxfA1Qet
BlbN7JBuuVRbUGOjkrYCOHHSwWdOoQLRXZ0VvwRyt/nRVxkhF9u+B7ESg5vgmkj0
m/g0qWVl5uDhH+R4wRFkTp4sgQKBgQDMP0VHmPefvYhmIJsRnKPPq8934H//9ntj
KC+qgEOmXthIESgq7IIkLU1AEyMg3GTeHfGFKaC590TZTsXdKBDyk4KBampmOlqJ
6F3Z7LL2SEDjfd7q1qaiMWE8VoY5n0yeBGP3a8kx1l4QPiVlqAAnjFQjVP8OVHRK
H4jWg1gGGwKBgG9L1F5/Py9aQ73XXP24ZzAv49Y4re1O3rjmebznYNJAR/AQjLUt
jN+YCJlQed9krPQP6O+1gwy7Mx00qHWzLtJeXyZ7clnAqr91fsceG4fhMfAVQvyT
9jENP09TCcgEUHZS0d0z1O18PgVqx8KBf3zfJgwluBQZOJk+PvUiDtMBAoGAG9PO
+5dCvZfUGp38oqSlyOiOprWGmvgZDYRvIhrBnMw1SXRU9awtewJVERLKaXK9p2mK
+2qVhGDFF5pc6OnJjVM9p6Y5At6WcU2gY82VAGsNtDD1CyfdoiF6Lv9W4Ejs5maQ
vCgKTGj5fdsJu1TtWHYNDJdK/MfS5DpxqVBR0NsCgYBqLps6f/ZnHkam5xhcXwuF
eQILtW470j00ZiY8eW3vsO3jDaJY4kKDqCg/vJvCvrJeI0Arz5xfK3U+0nT4/3OM
BxKm9KiZFy1FNMc4pQeJN4u1M60McZREw//ncU+B/lOTQ2RhOEDB79+nKhuezjkC
vI9mk1tEhXtadS6g1C83zQ==
-----END PRIVATE KEY-----
19 changes: 19 additions & 0 deletions packages/core/test/fixture/tls/localhost.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDJTCCAg2gAwIBAgIUTKIB22rVLojTO840h8mzTuRnOxIwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MTAyODA4MzQ0N1oXDTM2MTAy
NTA4MzQ0N1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA27Z4pxp0GVVWzwFQT4I+AGNb13g8tRSoOK7hvZ7TFH2v
kcBC+N7rrdlH/5a2lFGdoFKV0Mo2eV1Sge4pHqohzeadCSc/aYQEUJc6hmHVmZ8q
OIgSZJU/JsMRr7J8cBOm9saulzJh2KIqiRnqukBScob3A8ILwQXCbi/cHJIXV2tc
f/Lum7g9QPJvahQ1zOVUuasiYrKwaeKI22WoBnYKJQbXTx53vJPsCtRVl4sSCkFK
Bw08KlLAgxm3iMF/W1UgwLQNRCVwCzHg3IMM7Cufk/ZdYrq79W+dPQtO1hSwXtx/
cNzEM33+6HxMQbJqt0SD6f7fIGFTtqhLEd6RoZTzgwIDAQABo28wbTAdBgNVHQ4E
FgQU5yrZI6/BOM0LRqzxe/8i28xbj1gwHwYDVR0jBBgwFoAU5yrZI6/BOM0LRqzx
e/8i28xbj1gwDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARhwR/AAABgglsb2Nh
bGhvc3QwDQYJKoZIhvcNAQELBQADggEBALGiM6XviFYv4a75Om8rPGFiLD9DNPe0
Rxr0gOpaMvXheJQTMFkrM8sBqt0Y8gL2JT8ZYcEKXZSlh6n0cGvh5wyPsvsNA7A1
kABIfh0Yf2Znp2E/mgpF4kdRWgbRvv+77llTBJNdBViGIWNHzZH92q1UGWPs9JKD
sttvK02rP+Xt7GXqMERwlAvAC0RZ6RpV46QfG0SuKCnV369WQRDt5BtilRaePnFk
U4dq7nK2dq+rWu6OuysOQHyeEP7FBhxdQlTnpjXnOmAjcZihfm/Hl2cvubhlvBM5
yfO7vILtaXtrd82xBmUGOSzuRtBPwlsbXkhz/pupxJZtC4xhO9t+Qgo=
-----END CERTIFICATE-----
Loading
Loading