Skip to content

Commit

Permalink
Wildcard and error improvements with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminleonard committed Nov 27, 2024
1 parent 4a9899d commit 53a6a9a
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 6 deletions.
68 changes: 68 additions & 0 deletions app/components/form/fields/TlsCertsField.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
import { describe, expect, it } from 'vitest'

import { matchesDomain, parseCertificate } from './TlsCertsField'

describe('matchesDomain', () => {
it('matches wildcard subdomains', () => {
expect(matchesDomain('*.example.com', 'sub.example.com')).toBe(true)
expect(matchesDomain('*.example.com', 'example.com')).toBe(false)
expect(matchesDomain('*', 'any.domain')).toBe(false)
})

it('matches exact matches', () => {
expect(matchesDomain('example.com', 'example.com')).toBe(true)
expect(matchesDomain('example.com', 'www.example.com')).toBe(false)
})

it('matches multiple subdomains', () => {
expect(matchesDomain('*.example.com', 'sub.sub.example.com')).toBe(true)
expect(matchesDomain('*.example.com', 'sub.sub.sub.example.com')).toBe(true)
})

it('matches with case insensitivity', () => {
expect(matchesDomain('EXAMPLE.COM', 'example.com')).toBe(true)
expect(matchesDomain('example.com', 'EXAMPLE.COM')).toBe(true)
})

it('does not match incorrect wildcards', () => {
expect(matchesDomain('test.*', 'test.com')).toBe(false)
expect(matchesDomain('test.*', 'test.net')).toBe(false)
})
})

describe('parseCertificate', () => {
const validCert = `-----BEGIN CERTIFICATE-----\nMIIDbjCCAlagAwIBAgIUVF36cv2UevtKOGWP3GNV1h+TpScwDQYJKoZIhvcNAQEL\nBQAwGzEZMBcGA1UEAwwQdGVzdC5leGFtcGxlLmNvbTAeFw0yNDExMjcxNDE4MTha\nFw0yNTExMjcxNDE4MThaMBsxGTAXBgNVBAMMEHRlc3QuZXhhbXBsZS5jb20wggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0cBavU9cnrTY7CaOsHdfzr7e4\nmT7eRCGJa1jmuGeADGIs1IcMr/7jgiKS/1P69SehfqpFWXKAYn5OH+ickZfs55AB\nuyfh+KogmTkX6I40CnP9GohfgAaDVr119a2kdJNvinsCjNGfulMBYiw+sJBp4l/c\nzQRYMXaMk1ARKBgUuVZHZXnkWQKjp/GAQjVsUjl/dnBVeUuS4/0OVTLL8U6mGzdy\nf5s03bpBLOOJ9Owg1We5urYA6glCvvMh1VhBPsCnHFj6aYLnnWpJkVuJEKA+znEU\nU2n6T0bQorzVnn5ROtAn3ao4sGIVMbMeIaEvUt3zyVk+gtUvqSTPChFde6/LAgMB\nAAGjgakwgaYwHQYDVR0OBBYEFFzp73YRPxxu4bTQvmJy5rqHNXh7MB8GA1UdIwQY\nMBaAFFzp73YRPxxu4bTQvmJy5rqHNXh7MA8GA1UdEwEB/wQFMAMBAf8wUwYDVR0R\nBEwwSoIQdGVzdC5leGFtcGxlLmNvbYISKi50ZXN0LmV4YW1wbGUuY29tghEqLmRl\ndi5leGFtcGxlLmNvbYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB\nAQCstbMiTwHuSlwuUslV9SxewdxTtKAjNgUnCn1Jv7hs44wNTBqvMzDq2HB26wRR\nOnbt6gReOj9GdSRmJPNcgouaAGJWCXuaZPs34LgRJir6Z0FVcK7/O3SqfTOg3tJg\ngzg4xmtzXc7Im4VgvaLS5iXCOvUaKf/rXeYDa3r37EF+vyzcETt5bXwtU8BBFvVT\nJfPDla5lYv0h9Z+XsYEAqtbChdy+fVuHnF+EygZCT9KVFBPWQrsaF1Qc/CvP/+LM\nCrdLoB+2pkWbX075tv8LIbL2dW5Gzyw+lU6lzPL9Vikm3QXGRklKHA4SVuZ3F9tr\nwPRLWb4aPmo1COkgvg3Moqdw\n-----END CERTIFICATE-----`

const invalidCert = 'not-a-certificate'

it('parses valid certificate', () => {
const result = parseCertificate(validCert)
expect(result).toEqual({
commonName: ['test.example.com'],
subjectAltNames: [
'test.example.com',
'*.test.example.com',
'*.dev.example.com',
'localhost',
'127.0.0.1',
],
isValid: true,
})
})

it('returns invalid for invalid certificate', () => {
const result = parseCertificate(invalidCert)
expect(result).toEqual({
commonName: [],
subjectAltNames: [],
isValid: false,
})
})
})
60 changes: 54 additions & 6 deletions app/components/form/fields/TlsCertsField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,41 +181,89 @@ const validateCertificate = async (file: File) => {
return parseCertificate(await file.text())
}

function parseCertificate(certPem: string) {
export function parseCertificate(certPem: string) {
try {
const cert = new X509Certificate(certPem)
const nameItems = cert.getExtension(SubjectAlternativeNameExtension)?.names.items || []
return {
commonName: cert.subjectName.getField('CN') || [],
subjectAltNames: nameItems.map((item) => item.value) || [],
isValid: true,
}
} catch {
return null
return {
commonName: [],
subjectAltNames: [],
isValid: false,
}
}
}

function matchesDomain(pattern: string, domain: string): boolean {
export function matchesDomain(pattern: string, domain: string): boolean {
const patternParts = pattern.split('.')
const domainParts = domain.split('.')

if (patternParts.length !== domainParts.length) return false
// unsure if this would be an issue but we reject it anyway
if (pattern === '*') {
return false
}

if (patternParts[0] === '*') {
// if the pattern starts with a wildcard
const patternSuffix = patternParts.slice(1).join('.')
// we want wildcard domains to have same number or more parts
// and must match the pattern suffix exactly
return domainParts.length >= patternParts.length && domain.endsWith(patternSuffix)
}

return patternParts.every(
(part, i) => part === '*' || part.toLowerCase() === domainParts[i].toLowerCase()
// parts must match exactly for non-wildcard patterns
return (
patternParts.length === domainParts.length &&
patternParts.every((part, i) => part.toLowerCase() === domainParts[i].toLowerCase())
)
}

function CertDomainNotice({
commonName = [],
subjectAltNames = [],
isValid = true,
siloName,
domain,
}: {
commonName?: string[]
subjectAltNames?: string[]
isValid?: boolean
siloName: string
domain: string
}) {
if (!isValid) {
return (
<Message
variant="info"
title="Could not be parsed"
content={
<div className="flex flex-col space-y-2">
<div>
Certificate may not be valid, a silo expects a X.509 cert in PEM format.
</div>
<div>
Learn more about{' '}
<a
target="_blank"
rel="noreferrer"
href={links.systemSiloDocs} // would need updating
className="inline-flex items-center underline"
>
silo certs
<OpenLink12Icon className="ml-1" />
</a>
</div>
</div>
}
/>
)
}

if (commonName.length === 0 && subjectAltNames.length === 0) {
return null
}
Expand Down

0 comments on commit 53a6a9a

Please sign in to comment.