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: adding OCSP revocation implementation #134

Merged
merged 32 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
65ead65
Adding OCSP revocation implementation
kody-kimberl Mar 24, 2023
1150936
Updated revocation checks with chain validationand better func docs
kody-kimberl Mar 27, 2023
36890d6
Refactoring and addressing PR comments
kody-kimberl Mar 29, 2023
de36fc7
removing logger, moving chain validation, and adding new error
kody-kimberl Mar 29, 2023
7ca76d7
refactoring and updating based on PR comments
kody-kimberl Mar 30, 2023
115aef8
Refactoring certOCSPStatus loop and changing naming
kody-kimberl Mar 31, 2023
8f21073
changing names and refactoring cert status checks
kody-kimberl Apr 4, 2023
e0bf125
Merge branch 'main' into main
kody-kimberl Apr 5, 2023
b5b85fe
Updating constructor to return error for nil httpClient
kody-kimberl Apr 6, 2023
b89c7b5
Switching to notation-core-go/x509 for chain validation
kody-kimberl Apr 6, 2023
6057cd4
Changing return type of Validate to a new struct
kody-kimberl Apr 7, 2023
0d7af29
Addressing new PR comments
kody-kimberl Apr 10, 2023
a5d4259
Updating comments and ocsp package return types
kody-kimberl Apr 11, 2023
7d0e731
Adding NonRevokable Result and changing single cert behavior
kody-kimberl Apr 11, 2023
b330009
Updated results, reformatted errors, and changed single cert logic
kody-kimberl Apr 12, 2023
821d32d
Added error to returns
kody-kimberl Apr 12, 2023
ae7cccb
Cleaned up code a bit from comment suggestions
kody-kimberl Apr 13, 2023
b8bf5ff
Added and modified some documentation
kody-kimberl Apr 13, 2023
5757f70
Renamed base package to result
kody-kimberl Apr 14, 2023
5102a77
Added server URI to ServerResult and adjusted some errors
kody-kimberl Apr 14, 2023
1020040
Removing waterfall logic for multiple server results
kody-kimberl Apr 14, 2023
f291015
Addressing new PR comments
kody-kimberl Apr 17, 2023
dd0b168
Switching to invalidityDate
kody-kimberl Apr 18, 2023
5d6f6e8
Name changes and addressing comments
kody-kimberl Apr 18, 2023
6376931
Refactoring extension handling and zero time
kody-kimberl Apr 18, 2023
17c0ff7
Adding authentic signing time implementation
kody-kimberl Apr 18, 2023
6473871
Updating test certs, chain validation, and authentic signing time imp…
kody-kimberl Apr 18, 2023
ffcd32e
Addressed new comments
kody-kimberl Apr 19, 2023
03f6251
Updating to bypass linter warnings without comments
kody-kimberl Apr 19, 2023
75a41e1
Updating GetAuthenticSigningTime for TSA to return error for not impl…
kody-kimberl Apr 19, 2023
3645077
Updating GetAuthenticSigningTime generic error message
kody-kimberl Apr 19, 2023
2a643ff
Changed GetAuthenticSigningTime to AuthenticSigningTime
kody-kimberl Apr 20, 2023
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 @@ -6,6 +6,7 @@ require (
github.com/fxamacker/cbor/v2 v2.4.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/veraison/go-cose v1.0.0
golang.org/x/crypto v0.7.0
)

require github.com/x448/float16 v0.8.4 // indirect
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ github.com/veraison/go-cose v1.0.0 h1:Jxirc0rl3gG7wUFgW+82tBQNeK8T8e2Bk1Vd298ob4
github.com/veraison/go-cose v1.0.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
44 changes: 44 additions & 0 deletions revocation/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package revocation

import "fmt"

// RevokedInOCSPError is returned when the certificate's status for OCSP is ocsp.Revoked
type RevokedInOCSPError struct{}

func (e RevokedInOCSPError) Error() string {
return "certificate is revoked via OCSP"
}

// UnknownInOCSPError is returned when the certificate's status for OCSP is ocsp.Unknown
type UnknownInOCSPError struct{}

func (e UnknownInOCSPError) Error() string {
return "certificate has unknown status via OCSP"
}

// CheckOCSPError is returned when there is an error during the OCSP revocation check, not necessarily a revocation
type CheckOCSPError struct {
Err error
}

func (e CheckOCSPError) Error() string {
if e.Err != nil {
return fmt.Sprintf("error checking revocation via OCSP: %s", e.Err.Error())
} else {
return "error checking revocation via OCSP"
}
}

// NoOCSPServerError is returned when the OCSPServer is not specified or is not an HTTP URL.
type NoOCSPServerError struct{}

func (e NoOCSPServerError) Error() string {
return "no valid OCSPServer found"
}

// OCSPTimeoutError is returned when the connection attempt to an OCSP URL exceeds the specified threshold
type OCSPTimeoutError struct{}

func (e OCSPTimeoutError) Error() string {
return "exceeded timeout threshold for OCSP check"
}
63 changes: 63 additions & 0 deletions revocation/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package revocation

import (
"errors"
"testing"
)

func TestRevokedInOCSPError(t *testing.T) {
err := &RevokedInOCSPError{}
expectedMsg := "certificate is revoked via OCSP"

if err.Error() != expectedMsg {
t.Errorf("Expected %v but got %v", expectedMsg, err.Error())
}
}

func TestUnknownInOCSPError(t *testing.T) {
err := &UnknownInOCSPError{}
expectedMsg := "certificate has unknown status via OCSP"

if err.Error() != expectedMsg {
t.Errorf("Expected %v but got %v", expectedMsg, err.Error())
}
}

func TestCheckOCSPError(t *testing.T) {
t.Run("without_inner_error", func(t *testing.T) {
err := &CheckOCSPError{}
expectedMsg := "error checking revocation via OCSP"

if err.Error() != expectedMsg {
t.Errorf("Expected %v but got %v", expectedMsg, err.Error())
}
})

t.Run("with_inner_error", func(t *testing.T) {
err := &CheckOCSPError{Err: errors.New("inner error")}
expectedMsg := "error checking revocation via OCSP: inner error"

if err.Error() != expectedMsg {
t.Errorf("Expected %v but got %v", expectedMsg, err.Error())
}
})

}

func TestNoOCSPServerError(t *testing.T) {
err := &NoOCSPServerError{}
expectedMsg := "no valid OCSPServer found"

if err.Error() != expectedMsg {
t.Errorf("Expected %v but got %v", expectedMsg, err.Error())
}
}

func TestOCSPTimeoutError(t *testing.T) {
err := &OCSPTimeoutError{}
expectedMsg := "exceeded timeout threshold for OCSP check"

if err.Error() != expectedMsg {
t.Errorf("Expected %v but got %v", expectedMsg, err.Error())
}
}
249 changes: 249 additions & 0 deletions revocation/revocation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package revocation

import (
"bytes"
"crypto"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"sync"
"time"

"golang.org/x/crypto/ocsp"
)

// Revocation is an internal struct used for revocation checking
type revocation struct {
httpClient *http.Client
wg sync.WaitGroup
mergeErrorsFunc func(errors []error) error
}

// NewRevocation constructs a revocation object and substitutes default values for any that are passed as nil
func NewRevocation(httpClient *http.Client) *revocation {
if httpClient != nil {
return &revocation{httpClient: httpClient, wg: sync.WaitGroup{}, mergeErrorsFunc: defaultMergeErrors}
}
return &revocation{httpClient: http.DefaultClient, wg: sync.WaitGroup{}, mergeErrorsFunc: defaultMergeErrors}
}

// Validate checks OCSP, then CRL status, returns nil if all certs in the chain are not revoked.
// If there is an error, it will return one of the errors defined in this package in errors.go.
// (e.g. if a certificate in the chain is revoked by OCSP and there are no other errors, it will return revocation.RevokedInOCSPError)
//
// NOTE: Will only perform OCSP until CRL is implemented
func (r *revocation) Validate(certChain []*x509.Certificate) error {
ocspErr := r.OCSPStatus(certChain)
if ocspErr != nil {
return ocspErr
}
return nil // This will eventually return the result of CRLStatus
}

// OCSPStatus checks OCSP, returns nil if all certs in the chain are not revoked.
// If there is an error, it will return one of the errors defined in this package in errors.go.
// (e.g. if a certificate in the chain is revoked by OCSP and there are no other errors, it will return revocation.RevokedInOCSPError)
func (r *revocation) OCSPStatus(certChain []*x509.Certificate) error {
certResults := make([]error, len(certChain))
for i, cert := range certChain {
if i != (len(certChain) - 1) {
if !isCorrectIssuer(cert, certChain[i+1]) {
return CheckOCSPError{Err: errors.New("invalid chain: expected chain to be correct and complete with each cert issued by the next in the chain")}
}
r.wg.Add(1)
// Assume cert chain is accurate and next cert in chain is the issuer
go r.certOCSPStatus(cert, certChain[i+1], &certResults, i)
} else {
if !isCorrectIssuer(cert, cert) {
return CheckOCSPError{Err: errors.New("invalid chain: expected chain to end with root cert")}
}
// Last is root cert, which will never be revoked by OCSP
certResults[len(certChain)-1] = nil
}
}

r.wg.Wait()
return r.mergeErrorsFunc(certResults)
}

func isCorrectIssuer(subject *x509.Certificate, issuer *x509.Certificate) bool {
if err := subject.CheckSignatureFrom(issuer); err != nil {
return false
}
if !bytes.Equal(issuer.RawSubject, subject.RawIssuer) {
return false
}
return true
}

func (r *revocation) certOCSPStatus(cert *x509.Certificate, issuer *x509.Certificate, results *[]error, resultIndex int) error {
defer r.wg.Done()
var err error

ocspURLs := cert.OCSPServer
if len(ocspURLs) == 0 {
// OCSP not enabled for this certificate.
(*results)[resultIndex] = NoOCSPServerError{}
return NoOCSPServerError{}
}

serverErrs := make([]error, len(ocspURLs))
for serverIndex, server := range ocspURLs {
resp, err := r.ocspRequest(cert, issuer, server)
if err != nil {
// If there is a server error, attempt all servers before determining what to return to the user
serverErrs[serverIndex] = err
continue
}

if time.Now().After(resp.NextUpdate) {
err = errors.New("expired OCSP response")
serverErrs[serverIndex] = err
continue
}

foundNoCheck := false
pkixNoCheckOID := "1.3.6.1.5.5.7.48.1.5"
for _, extension := range resp.Extensions {
if !foundNoCheck && extension.Id.String() == pkixNoCheckOID {
foundNoCheck = true
}
}
if !foundNoCheck {
// This will be ignored until CRL is implemented
// If it isn't found, CRL should be used to verify the OCSP response
fmt.Printf("\n[WARNING] An ocsp signing cert is missing the id-pkix-ocsp-nocheck extension (%s)\n", pkixNoCheckOID)
}

if resp.Status == ocsp.Revoked {
if time.Now().After(resp.RevokedAt) {
(*results)[resultIndex] = RevokedInOCSPError{}
return RevokedInOCSPError{}
} else {
(*results)[resultIndex] = nil
return nil
}
} else if resp.Status == ocsp.Good {
(*results)[resultIndex] = nil
return nil
} else {
(*results)[resultIndex] = UnknownInOCSPError{}
return UnknownInOCSPError{}
}

}
// Errors in all server responses, determine the most pressing
err = r.mergeErrorsFunc(serverErrs)
(*results)[resultIndex] = err
return err
}

func (r *revocation) ocspRequest(cert, issuer *x509.Certificate, server string) (*ocsp.Response, error) {
ocspRequest, err := ocsp.CreateRequest(cert, issuer, &ocsp.RequestOptions{Hash: crypto.SHA1})
if err != nil {
return nil, err
}

var resp *http.Response
if len(ocspRequest) > 256 {
buf := bytes.NewBuffer(ocspRequest)
resp, err = r.httpClient.Post(server, "application/ocsp-request", buf)
} else {
reqURL := server + "/" + url.QueryEscape(base64.StdEncoding.EncodeToString(ocspRequest))
resp, err = r.httpClient.Get(reqURL)
}

if err != nil {
if urlErr, ok := err.(*url.Error); ok && urlErr.Timeout() {
return nil, OCSPTimeoutError{}
}
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("failed to retrieve OSCP: response had status code %d", resp.StatusCode)
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

switch {
case bytes.Equal(body, ocsp.UnauthorizedErrorResponse):
return nil, errors.New("OSCP unauthorized")
case bytes.Equal(body, ocsp.MalformedRequestErrorResponse):
return nil, errors.New("OSCP malformed")
case bytes.Equal(body, ocsp.InternalErrorErrorResponse):
return nil, errors.New("OSCP internal error")
case bytes.Equal(body, ocsp.TryLaterErrorResponse):
return nil, errors.New("OSCP try later")
case bytes.Equal(body, ocsp.SigRequredErrorResponse):
return nil, errors.New("OSCP signature required")
}

return ocsp.ParseResponseForCert(body, cert, issuer)
}

// SetMergeErrorsFunction allows you to specify an alternative function to merge errors if the default does not fit your use case. You can also pass nil to reset it to the defaultMergeErrors function
func (r *revocation) SetMergeErrorsFunction(mergeErrorsFunc func(errors []error) error) {
if mergeErrorsFunc == nil {
r.mergeErrorsFunc = defaultMergeErrors
} else {
r.mergeErrorsFunc = mergeErrorsFunc
}
}

// DefaultMergeErrors condenses errors for a list of errors (either for cert chain or OCSP servers) into one primary error
func defaultMergeErrors(errorList []error) error {
var result error
if len(errorList) > 0 {
result = errorList[0]

for _, err := range errorList {
if err == nil {
continue
}
switch t := err.(type) {
case RevokedInOCSPError:
// There is a revoked certificate
// return since any cert being revoked means leaf is revoked
return t
case CheckOCSPError:
// There is an error checking
// return since any cert having error means chain has error (return earliest)
return t
case UnknownInOCSPError:
// A cert in the chain has status unknown
// will not return immediately (in case one is revoked or has error), but will override other chain errors
result = t
case NoOCSPServerError:
// A cert in the chain does not have OCSP enabled
// Still considered valid and not revoked
// will not return immediately (in case there is higher level error)
// will override OCSPTimeoutError and nil, but not UnknownInOCSPError (since a known unknown is worse than a cert without OCSP)
if _, ok := result.(UnknownInOCSPError); !ok || result == nil {
result = t
}
case OCSPTimeoutError:
// A cert in the chain timed out while checking OCSP
// will not return immediately (in case there is higher level error)
// will override nil, but not UnknownInOCSPError or NoOCSPServerError (since timeout should only be conveyed if that is the only issue)
if result == nil {
result = t
}
default:
return CheckOCSPError{Err: err}
}
}

return result
} else {
return nil
}
}
Loading