@ -20,6 +20,7 @@ package client
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
"os"
@ -33,6 +34,7 @@ import (
"git.cacert.org/cacert-gosigner/pkg/messages"
"git.cacert.org/cacert-gosigner/pkg/protocol"
"git.cacert.org/cacert-gosignerclient/internal/command"
"git.cacert.org/cacert-gosignerclient/internal/config"
)
@ -49,30 +51,43 @@ type Profile struct {
}
type CertInfo struct {
Name string
FetchCRL bool
Name string
FetchCert bool
FetchCRL bool
LastKnownCRL * big . Int
Certificate * x509 . Certificate
Profiles map [ string ] * Profile
}
type SignerInfo struct {
SignerHealth bool
SignerVersion string
CACertificates [ ] CertInfo
UsableProfiles map [ string ] [ ] Profile
CACertificates [ ] string
}
func ( i * SignerInfo ) containsCA ( caName string ) bool {
for _ , name := range i . CACertificates {
if name == caName {
return true
}
}
return false
}
type Client struct {
port * serial . Port
logger * logrus . Logger
framer protocol . Framer
in chan [ ] byte
out chan [ ] byte
commands chan * protocol . Command
handler protocol . ClientHandler
config * config . ClientConfig
signerInfo * SignerInfo
callback chan interface { }
port * serial . Port
logger * logrus . Logger
framer protocol . Framer
in chan [ ] byte
out chan [ ] byte
commands chan * protocol . Command
handler protocol . ClientHandler
config * config . ClientConfig
signerInfo * SignerInfo
knownCertificates map [ string ] * CertInfo
callback chan interface { }
sync . Mutex
lastKnownCRLS map [ string ] * big . Int
}
func ( c * Client ) Run ( ctx context . Context ) error {
@ -158,63 +173,118 @@ func (c *Client) commandLoop(ctx context.Context) {
fetchCRLTimer := time . NewTimer ( c . config . FetchCRLStart )
for {
newCommands := make ( [ ] * protocol . Command , 0 )
select {
case <- ctx . Done ( ) :
return
case callbackData := <- c . callback :
err := c . handleCallback ( callbackData )
addCommands, err := c . handleCallback ( callbackData )
if err != nil {
c . logger . WithError ( err ) . Error ( "callback handling failed" )
}
newCommands = append ( newCommands , addCommands ... )
case <- fetchCRLTimer . C :
for _ , crlInfo := range c . buildCRLInfo ( ) {
var lastKnown [ ] byte
if crlInfo . LastKnown != nil {
lastKnown = crlInfo . LastKnown . Bytes ( )
}
c . commands <- & protocol . Command {
Announce : messages . BuildCommandAnnounce ( messages . CmdFetchCRL ) ,
Command : & messages . FetchCRLCommand {
IssuerID : crlInfo . Name ,
LastKnownID : lastKnown ,
} ,
}
for _ , crlInfo := range c . requiredCRLs ( ) {
newCommands = append ( newCommands , command . FetchCRL ( crlInfo . Name , crlInfo . LastKnown ) )
}
fetchCRLTimer . Reset ( c . config . FetchCRLInterval )
case <- healthTimer . C :
c . commands <- & protocol . Command {
Announce : messages . BuildCommandAnnounce ( messages . CmdHealth ) ,
Command : & messages . HealthCommand { } ,
}
newCommands = append ( newCommands , command . Health ( ) )
healthTimer . Reset ( c . config . HealthInterval )
}
for _ , nextCommand := range newCommands {
select {
case <- ctx . Done ( ) :
return
case c . commands <- nextCommand :
c . logger . WithField ( "command" , nextCommand . Announce ) . Trace ( "sent command" )
}
}
}
}
func ( c * Client ) handleCallback ( data interface { } ) error {
func ( c * Client ) handleCallback ( data interface { } ) ( [ ] * protocol . Command , error ) {
switch d := data . ( type ) {
case SignerInfo :
c . updateSignerInfo ( d )
return c . updateSignerInfo ( d )
case * messages . CAInfoResponse :
return c . updateCAInformation ( d )
case * messages . FetchCRLResponse :
c . updateCRL ( d )
return c . updateCRL ( d )
default :
return fmt . Errorf ( "unknown callback data of type %T" , data )
return nil , fmt . Errorf ( "unknown callback data of type %T" , data )
}
}
return nil
func ( c * Client ) updateSignerInfo ( signerInfo SignerInfo ) ( [ ] * protocol . Command , error ) {
c . logger . Debug ( "update signer info" )
c . Lock ( )
c . signerInfo = & signerInfo
c . Unlock ( )
c . learnNewCACertificates ( )
c . forgetRemovedCACertificates ( )
newCommands := make ( [ ] * protocol . Command , 0 )
for _ , caName := range c . requiredCertificateInfo ( ) {
newCommands = append ( newCommands , command . CAInfo ( caName ) )
}
return newCommands , nil
}
func ( c * Client ) updateSignerInfo ( signerInfo SignerInfo ) {
func ( c * Client ) update CAInformation( d * messages . CAInfoResponse ) ( [ ] * protocol . Command , error ) {
c . Lock ( )
defer c . Unlock ( )
c . logger . Debug ( "update signer info" )
caInfo , ok := c . knownCertificates [ d . Name ]
if ! ok {
c . logger . WithField ( "certificate" , d . Name ) . Warn ( "unknown CA certificate" )
c . signerInfo = & signerInfo
return nil , nil
}
cert , err := x509 . ParseCertificate ( d . Certificate )
if err != nil {
return nil , fmt . Errorf ( "could not parse CA certificate for %s: %w" , d . Name , err )
}
if ! cert . IsCA {
return nil , fmt . Errorf ( "certificate for %s is not a CA certificate" , d . Name )
}
err = c . writeCertificate ( caInfo . Name , d . Certificate )
if err != nil {
c . logger . WithError ( err ) . WithField ( "certificate" , d . Name ) . Warn ( "could not write CA certificate files" )
}
caInfo . Certificate = cert
caInfo . FetchCert = false
caInfo . Profiles = make ( map [ string ] * Profile )
for _ , p := range d . Profiles {
caInfo . Profiles [ p . Name ] = & Profile {
Name : p . Name ,
UseFor : p . UseFor . String ( ) ,
}
}
if len ( cert . CRLDistributionPoints ) == 0 {
caInfo . FetchCRL = false
return nil , nil
}
return [ ] * protocol . Command { command . FetchCRL ( caInfo . Name , c . lastKnownCRL ( caInfo ) ) } , nil
}
type CRLInfo struct {
@ -222,43 +292,63 @@ type CRLInfo struct {
LastKnown * big . Int
}
func ( c * Client ) buildCRLInfo ( ) [ ] CRLInfo {
func ( c * Client ) requiredCRLs ( ) [ ] CRLInfo {
c . Lock ( )
defer c . Unlock ( )
if c . signerInfo == nil {
c . logger . Warn ( "no signer info available ")
if c . knownCertificates == nil {
c . logger . Warn ( "no certificates known ")
return nil
}
infos := make ( [ ] CRLInfo , 0 )
for _ , caInfo := range c . signerInfo. CA Certificates {
for _ , caInfo := range c . known Certificates {
if caInfo . FetchCRL {
lastKnown := c . lastKnownCRL ( caInfo . Name )
infos = append ( infos , CRLInfo { Name : caInfo . Name , LastKnown : c . lastKnownCRL ( caInfo ) } )
}
}
return infos
}
func ( c * Client ) requiredCertificateInfo ( ) [ ] string {
c . Lock ( )
defer c . Unlock ( )
if c . knownCertificates == nil {
c . logger . Warn ( "no certificates known" )
return nil
}
infos := make ( [ ] string , 0 )
infos = append ( infos , CRLInfo { Name : caInfo . Name , LastKnown : lastKnown } )
for _ , caInfo := range c . knownCertificates {
if caInfo . FetchCert {
infos = append ( infos , caInfo . Name )
}
}
return infos
}
func ( c * Client ) lastKnownCRL ( caName string ) * big . Int {
func ( c * Client ) lastKnownCRL ( caInfo * CertInfo ) * big . Int {
caName := caInfo . Name
crlFileName := c . buildCRLFileName ( caName )
_ , err := os . Stat ( crlFileName )
if err != nil {
c . logger . WithField ( "crl" , crlFileName ) . Debug ( "CRL file does not exist" )
delete ( c . lastKnownCRLS , caName )
return nil
}
lastKnown , ok := c . lastKnownCRLS [ caName ]
if ! ok {
lastKnown := caInfo . LastKnownCRL
if lastKnown == nil {
derData , err := os . ReadFile ( crlFileName )
if err != nil {
c . logger . WithError ( err ) . WithField ( "crl" , crlFileName ) . Error ( "could not read CRL data" )
@ -274,27 +364,27 @@ func (c *Client) lastKnownCRL(caName string) *big.Int {
}
lastKnown = crl . Number
c . lastKnownCRLS [ caName ] = lastKnown
}
return lastKnown
}
func ( c * Client ) buildCRLFileName ( caName string ) string {
return path . Join ( c . config . CRLDirectory , fmt . Sprintf ( "%s.crl" , caName ) )
}
func ( c * Client ) updateCRL ( d * messages . FetchCRLResponse ) {
func ( c * Client ) updateCRL ( d * messages . FetchCRLResponse ) ( [ ] * protocol . Command , error ) {
var (
der [ ] byte
crlNumber * big . Int
der [ ] byte
err error
)
caInfo , ok := c . knownCertificates [ d . IssuerID ]
if ! ok {
c . logger . WithField ( "certificate" , d . IssuerID ) . Warn ( "unknown CA certificate" )
}
if d . UnChanged {
c . logger . WithField ( "issuer" , d . IssuerID ) . Debug ( "CRL did not change" )
return
return nil , nil
}
if ! d . IsDelta {
@ -304,29 +394,25 @@ func (c *Client) updateCRL(d *messages.FetchCRLResponse) {
if err != nil {
c . logger . WithError ( err ) . Error ( "CRL from signer could not be parsed" )
return
return nil , nil
}
crlNumber = list . Number
} else {
crlFileName := c . buildCRLFileName ( d . IssuerID )
der , err : = c . patchCRL ( crlFileName , d . CRLData )
der , err = c . patchCRL ( crlFileName , d . CRLData )
if err != nil {
c . logger . WithError ( err ) . Error ( "CRL patching failed" )
delete ( c . lastKnownCRLS , d . IssuerID )
return
return nil , nil
}
list , err := x509 . ParseRevocationList ( der )
if err != nil {
c . logger . WithError ( err ) . Error ( "could not parse patched CRL" )
delete ( c . lastKnownCRLS , d . IssuerID )
return
return nil , nil
}
crlNumber = list . Number
@ -335,17 +421,57 @@ func (c *Client) updateCRL(d *messages.FetchCRLResponse) {
if err := c . writeCRL ( d . IssuerID , der ) ; err != nil {
c . logger . WithError ( err ) . Error ( "could not store CRL" )
delete ( c . lastKnownCRLS , d . IssuerID )
caInfo . LastKnownCRL = nil
return nil , nil
}
caInfo . LastKnownCRL = crlNumber
return nil , nil
}
func ( c * Client ) buildCRLFileName ( caName string ) string {
return path . Join ( c . config . PublicDataDirectory , fmt . Sprintf ( "%s.crl" , caName ) )
}
return
func ( c * Client ) buildCertificateFileName ( caName string , certFormat string ) string {
return path . Join ( c . config . PublicDataDirectory , fmt . Sprintf ( "%s.%s" , caName , certFormat ) )
}
func ( c * Client ) ensurePublicDataDirectory ( ) error {
if err := os . MkdirAll ( c . config . PublicDataDirectory , worldReadableDirPerm ) ; err != nil {
return fmt . Errorf ( "could not create public CA data directory %s: %w" , c . config . PublicDataDirectory , err )
}
c . lastKnownCRLS [ d . IssuerID ] = crlNumber
return nil
}
func ( c * Client ) writeCertificate ( caName string , derBytes [ ] byte ) error {
if err := c . ensurePublicDataDirectory ( ) ; err != nil {
return err
}
if err := os . WriteFile (
c . buildCertificateFileName ( caName , "crt" ) , derBytes , worldReadableFilePerm ,
) ; err != nil {
c . logger . WithError ( err ) . Error ( "could not write DER encoded certificate file" )
}
pemBytes := pem . EncodeToMemory ( & pem . Block { Type : "CERTIFICATE" , Bytes : derBytes } )
if err := os . WriteFile (
c . buildCertificateFileName ( caName , "pem" ) , pemBytes , worldReadableFilePerm ,
) ; err != nil {
c . logger . WithError ( err ) . Error ( "could not write PEM encoded certificate file" )
}
return nil
}
func ( c * Client ) writeCRL ( caName string , crlBytes [ ] byte ) error {
if err := os . MkdirAll ( c . config . CRLDirectory , worldReadableDirPerm ) ; err != nil {
return fmt . Errorf ( "could not create CRL directory %s: %w" , c . config . CRLDirectory , err )
if err := c. ensurePublicDataDirectory ( ) ; err != nil {
return err
}
if err := os . WriteFile ( c . buildCRLFileName ( caName ) , crlBytes , worldReadableFilePerm ) ; err != nil {
@ -374,6 +500,38 @@ func (c *Client) patchCRL(crlFileName string, diff []byte) ([]byte, error) {
return der , nil
}
func ( c * Client ) learnNewCACertificates ( ) {
c . Lock ( )
defer c . Unlock ( )
for _ , caName := range c . signerInfo . CACertificates {
if _ , ok := c . knownCertificates [ caName ] ; ok {
continue
}
c . knownCertificates [ caName ] = & CertInfo {
Name : caName ,
FetchCert : true ,
FetchCRL : true ,
}
}
}
func ( c * Client ) forgetRemovedCACertificates ( ) {
c . Lock ( )
defer c . Unlock ( )
for knownCA := range c . knownCertificates {
if c . signerInfo . containsCA ( knownCA ) {
continue
}
c . logger . WithField ( "certificate" , knownCA ) . Warn ( "signer did not send status for certificate" )
delete ( c . knownCertificates , knownCA )
}
}
func New (
cfg * config . ClientConfig ,
logger * logrus . Logger ,
@ -387,15 +545,15 @@ func New(
}
client := & Client {
logger : logger ,
framer : cobsFramer ,
in : make ( chan [ ] byte ) ,
out : make ( chan [ ] byte ) ,
commands : commands ,
handler : handler ,
config : cfg ,
callback : callback ,
lastKnownCRLS: make ( map [ string ] * big . Int ) ,
logger : logger ,
framer : cobsFramer ,
in : make ( chan [ ] byte ) ,
out : make ( chan [ ] byte ) ,
commands : commands ,
handler : handler ,
config : cfg ,
callback : callback ,
knownCertificates: make ( map [ string ] * CertInfo ) ,
}
err = client . setupConnection ( & serial . Config {