Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: perform OCSP request from Certificate #134

Merged
merged 1 commit into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
github.com/tursodatabase/go-libsql v0.0.0-20240429120401-651096bbee0b
golang.org/x/crypto v0.17.0
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tursodatabase/go-libsql v0.0.0-20240429120401-651096bbee0b h1:R7hev4b96zgXjKbS2ZNbHBnDvyFZhH+LlMqtKH6hIkU=
github.com/tursodatabase/go-libsql v0.0.0-20240429120401-651096bbee0b/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
Expand Down
2 changes: 1 addition & 1 deletion internal/ports/models/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func (m BaseModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.prevState = m.state
m.state = certificateView
m.title = titles[certificateView]
m.certificateModel = NewCertificateModel(msg.Certificate, m.commands)
m.certificateModel = NewCertificateModel(msg.Certificate, msg.CertificateChain, m.commands)
}

return m.handleStates(msg)
Expand Down
62 changes: 49 additions & 13 deletions internal/ports/models/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package models
import (
"crypto/x509"
"strings"
"time"

"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
Expand All @@ -18,15 +19,17 @@ type certificateKeyMap struct {
Quit key.Binding
Home key.Binding
Search key.Binding
OSCP key.Binding
}

func (k *certificateKeyMap) ShortHelp() []key.Binding {
return []key.Binding{k.Search, k.Back, k.Quit, k.Home}
return []key.Binding{k.Search, k.OSCP, k.Back, k.Home}
}

func (k *certificateKeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.Search, k.Home},
{k.OSCP},
{k.Back, k.Quit},
}
}
Expand All @@ -48,24 +51,33 @@ var certificateKeys = certificateKeyMap{
key.WithKeys("s"),
key.WithHelp("s", "search CRLs with this certificate"),
),
OSCP: key.NewBinding(
key.WithKeys("o"),
key.WithHelp("o", "perform OCSP request"),
),
}

type CertificateModel struct {
keys certificateKeyMap
styles *styles.Styles
certificate *x509.Certificate // TODO create custom struct and move parsing to domain model to do advanced parsing
revocationInfo *crl.RevokedCertificate
foundOnCRL *bool
errorMsg string
commands *commands.Commands
keys certificateKeyMap
styles *styles.Styles
certificate *x509.Certificate // TODO create custom struct and move parsing to domain model to do advanced parsing, also consider removing certificate as this can be obtained from the chiain
certificateChain []*x509.Certificate
revocationInfo *crl.RevokedCertificate
foundOnCRL *bool
errorMsg string
OCSPStatus string
OCSPRevocationDate time.Time
OCSPRevocationReason string
commands *commands.Commands
}

func NewCertificateModel(cert *x509.Certificate, cmds *commands.Commands) *CertificateModel {
func NewCertificateModel(cert *x509.Certificate, certificateChain []*x509.Certificate, cmds *commands.Commands) *CertificateModel {
return &CertificateModel{
keys: certificateKeys,
styles: styles.DefaultStyles(),
certificate: cert,
commands: cmds,
keys: certificateKeys,
styles: styles.DefaultStyles(),
certificate: cert,
certificateChain: certificateChain,
commands: cmds,
}
}

Expand All @@ -83,10 +95,25 @@ func (c *CertificateModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "s":
cmd = c.commands.Search(c.certificate.SerialNumber.String())
return c, cmd
case "o":
if len(c.certificate.OCSPServer) == 0 {
c.errorMsg = "Certificate does not contain a OCSP Server"
return c, cmd
}

if len(c.certificateChain) <= 1 {
c.errorMsg = "Certificate does not contain a certificate chain, Isser certificate missing"
return c, cmd
}
cmd = c.commands.OCSPRequest(c.certificate, c.certificateChain[1], c.certificate.OCSPServer[0])
}
case messages.GetRevokedCertificateMsg:
c.revocationInfo = msg.RevokedCertificate
c.foundOnCRL = &msg.Found
case messages.OCSPResponseMsg:
c.OCSPStatus = msg.Status
c.OCSPRevocationDate = msg.RevocationDate
c.OCSPRevocationReason = msg.RevocationReason
}
return c, cmd
}
Expand Down Expand Up @@ -120,6 +147,15 @@ func (c *CertificateModel) View() string {
}
}

if c.OCSPStatus != "" {
s.WriteString("\n\nOCSP Response: \n")
s.WriteString(c.styles.RevokedCertificateText.Render("OCSP Status: ") + c.OCSPStatus)
if c.OCSPRevocationDate != (time.Time{}) {
s.WriteString(c.styles.RevokedCertificateText.Render("Revocation Reason: ") + c.OCSPRevocationReason)
s.WriteString(c.styles.RevokedCertificateText.Render("Revocation Date: ") + c.OCSPRevocationDate.Format(time.RFC3339))
}
}

certInfo := c.styles.Text.Render(s.String())

return lipgloss.JoinVertical(lipgloss.Top, certInfo)
Expand Down
130 changes: 130 additions & 0 deletions internal/ports/models/commands/ocsp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package commands

import (
"bytes"
"crypto"
"crypto/x509"
"errors"
"io"
"log"
"net/http"

tea "github.com/charmbracelet/bubbletea"
"github.com/pimg/certguard/internal/ports/models/messages"
"github.com/pimg/certguard/pkg/uri"
"golang.org/x/crypto/ocsp"
)

func (c *Commands) OCSPRequest(cert, issuerCert *x509.Certificate, ocspServerURL string) tea.Cmd {
opts := ocsp.RequestOptions{
Hash: crypto.SHA256,
}

return func() tea.Msg {
if cert == nil {
log.Printf("certificate is nil")
return messages.ErrorMsg{
Err: errors.New("certificate is nil"),
}
}

if issuerCert == nil {
log.Printf("certificate Issuer is nil")
return messages.ErrorMsg{
Err: errors.New("certificate Issuer is nil"),
}
}

OCSPServerURL, err := uri.ValidateURI(ocspServerURL)
if err != nil {
log.Printf("could not validate OCSP server URL: %s, err: %v", ocspServerURL, err)
return messages.ErrorMsg{
Err: errors.Join(errors.New("could not validate OCSP server URL"), err),
}
}

log.Printf("Querying OCSP server URL: %s, for certificate: %s", ocspServerURL, cert.SerialNumber.String())

buffer, err := ocsp.CreateRequest(cert, issuerCert, &opts)
if err != nil {
log.Printf("could not create OCSP request for certificate: %s", cert.SerialNumber.String())
return messages.ErrorMsg{
Err: errors.Join(errors.New("could not create OCSP request"), err),
}
}

httpRequest, err := http.NewRequest(http.MethodPost, OCSPServerURL.String(), bytes.NewReader(buffer))
if err != nil {
log.Printf("could not create OCSP request for certificate: %s, err: %v", cert.SerialNumber.String(), err)
return errors.Join(errors.New("could not create OCSP request"), err)
}

httpRequest.Header.Add("Content-Type", "application/ocsp-request")
httpRequest.Header.Add("Accept", "application/ocsp-response")
httpRequest.Header.Add("Host", OCSPServerURL.Hostname())

httpClient := &http.Client{}
httpResponse, err := httpClient.Do(httpRequest)
if err != nil {
log.Printf("could not send OCSP request for certificate: %s, err: %v", cert.SerialNumber.String(), err)
return messages.ErrorMsg{
Err: errors.Join(errors.New("could not send OCSP request"), err),
}
}
defer httpResponse.Body.Close()
OCSPResponseRaw, err := io.ReadAll(httpResponse.Body)
if err != nil {
log.Printf("could not read OCSP response for certificate: %s, err: %v", cert.SerialNumber.String(), err)
return messages.ErrorMsg{
Err: errors.Join(errors.New("could not read OCSP response for certificate"), err),
}
}

OCSPResponse, err := ocsp.ParseResponseForCert(OCSPResponseRaw, cert, issuerCert)
if err != nil {
log.Printf("could not parse OCSP response for certificate: %s, err: %v", cert.SerialNumber, err)
return messages.ErrorMsg{
Err: errors.Join(errors.New("could not parse OCSP response for certificate"), err),
}
}

switch OCSPResponse.Status {
case ocsp.Good:
return messages.OCSPResponseMsg{Status: "Good"}
case ocsp.Revoked:
revocationReason := parseRevocationReason(OCSPResponse.RevocationReason)
return messages.OCSPResponseMsg{Status: "Revoked", RevocationDate: OCSPResponse.RevokedAt, RevocationReason: revocationReason}
case ocsp.Unknown:
return messages.OCSPResponseMsg{Status: "Unknown"}
default:
return messages.OCSPResponseMsg{Status: "Unknown"}
}
}
}

func parseRevocationReason(reason int) string {
switch reason {
case ocsp.Unspecified:
return "unspecified"
case ocsp.KeyCompromise:
return "key compromise"
case ocsp.CACompromise:
return "CA compromise"
case ocsp.AffiliationChanged:
return "affiliation changed"
case ocsp.Superseded:
return "superseded"
case ocsp.CessationOfOperation:
return "cessation of operation"
case ocsp.CertificateHold:
return "certificate hold"
case ocsp.RemoveFromCRL:
return "remove from CRL"
case ocsp.PrivilegeWithdrawn:
return "privilege-withdrawn"
case ocsp.AACompromise:
return "AA compromise"
default:
return "unknown"
}
}
Loading