summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ScanningParser.hs11
-rwxr-xr-xcert_valid.pl210
-rw-r--r--validatecert.hs222
3 files changed, 443 insertions, 0 deletions
diff --git a/ScanningParser.hs b/ScanningParser.hs
index a0a5d23..f99e120 100644
--- a/ScanningParser.hs
+++ b/ScanningParser.hs
@@ -3,6 +3,7 @@
3module ScanningParser 3module ScanningParser
4 ( ScanningParser(..) 4 ( ScanningParser(..)
5 , scanAndParse 5 , scanAndParse
6 , scanAndParse1
6 ) where 7 ) where
7 8
8import Data.Maybe 9import Data.Maybe
@@ -61,3 +62,13 @@ scanAndParse psr@(ScanningParser ffst pbdy) ts = do
61 b <- ffst x 62 b <- ffst x
62 return (b,drop 1 ts) 63 return (b,drop 1 ts)
63 64
65scanAndParse1 :: ScanningParser a c -> [a] -> (Maybe c, [a])
66scanAndParse1 psr@(ScanningParser ffst pbdy) ts =
67 maybe (Nothing,[]) (uncurry pbdy) mb
68 where
69 mb = listToMaybe $ mapMaybe findfst' tss
70 tss = tails ts
71 findfst' ts = do
72 x <- listToMaybe ts
73 b <- ffst x
74 return (b,drop 1 ts)
diff --git a/cert_valid.pl b/cert_valid.pl
new file mode 100755
index 0000000..41a1984
--- /dev/null
+++ b/cert_valid.pl
@@ -0,0 +1,210 @@
1#!/usr/bin/perl -w
2#
3# A dummy SSL certificate validator helper that
4# echos back all the SSL errors sent by Squid.
5#
6
7use warnings;
8use strict;
9use Getopt::Long;
10use Pod::Usage;
11use Crypt::OpenSSL::X509;
12use FileHandle;
13use POSIX qw(strftime);
14
15my $debug = 0;
16my $help = 0;
17
18=pod
19
20=head1 NAME
21
22cert_valid.pl - A fake cert validation helper for Squid
23
24=head1 SYNOPSIS
25
26cert_valid.pl [-d | --debug] [-h | --help]
27
28=over 8
29
30=item B<-h | --help>
31
32brief help message
33
34=item B<-d | --debug>
35
36enable debug messages to stderr
37
38=back
39
40=head1 DESCRIPTION
41
42Retrieves the SSL certificate error list from squid and echo back without any change.
43
44=head1 COPYRIGHT
45
46(C) 2012 The Measurement Factory, Author: Tsantilas Christos
47
48This program is free software. You may redistribute copies of it under the
49terms of the GNU General Public License version 2, or (at your opinion) any
50later version.
51
52=cut
53
54GetOptions(
55 'help' => \$help,
56 'debug' => \$debug,
57 ) or pod2usage(1);
58
59pod2usage(1) if ($help);
60
61$|=1;
62while (<>) {
63 my $first_line = $_;
64 my @line_args = split;
65
66 if ($first_line =~ /^\s*$/) {
67 next;
68 }
69
70 my $response;
71 my $haserror = 0;
72 my $channelId = $line_args[0];
73 my $code = $line_args[1];
74 my $bodylen = $line_args[2];
75 my $body = $line_args[3] . "\n";
76 if ($channelId !~ /\d+/) {
77 $response = $channelId." BH message=\"This helper is concurrent and requires the concurrency option to be specified.\"\1";
78 } elsif ($bodylen !~ /\d+/) {
79 $response = $channelId." BH message=\"cert validator request syntax error \" \1";
80 } else {
81 my $readlen = length($body);
82 my %certs = ();
83 my %errors = ();
84 my @responseErrors = ();
85
86 while($readlen < $bodylen) {
87 my $t = <>;
88 if (defined $t) {
89 $body = $body . $t;
90 $readlen = length($body);
91 }
92 }
93
94 print(STDERR logPrefix()."GOT ". "Code=".$code." $bodylen \n") if ($debug); #.$body;
95 my $hostname;
96 parseRequest($body, \$hostname, \%errors, \%certs);
97 print(STDERR logPrefix()."Parse result: \n") if ($debug);
98 print(STDERR logPrefix()."\tFOUND host:".$hostname."\n") if ($debug);
99 print(STDERR logPrefix()."\tFOUND ERRORS:") if ($debug);
100 foreach my $err (keys %errors) {
101 print(STDERR logPrefix().$errors{$err}{"name"}."/".$errors{$err}{"cert"}." ,") if ($debug);
102 }
103 print(STDERR "\n") if ($debug);
104 foreach my $key (keys %certs) {
105 ## Use "perldoc Crypt::OpenSSL::X509" for X509 available methods.
106 print(STDERR logPrefix()."\tFOUND cert ".$key.": ".$certs{$key}->subject() . "\n") if ($debug);
107 }
108
109 #got the peer certificate ID. Assume that the peer certificate is the first one.
110 my $peerCertId = (keys %certs)[0];
111
112 # Echo back the errors: fill the responseErrors array with the errors we read.
113 foreach my $err (keys %errors) {
114 $haserror = 1;
115 appendError (\@responseErrors,
116 $errors{$err}{"name"}, #The error name
117 "Checked by Cert Validator", # An error reason
118 $errors{$err}{"cert"} # The cert ID. We are always filling with the peer certificate.
119 );
120 }
121
122 $response = createResponse(\@responseErrors);
123 my $len = length($response);
124 if ($haserror) {
125 $response = $channelId." ERR ".$len." ".$response."\1";
126 } else {
127 $response = $channelId." OK ".$len." ".$response."\1";
128 }
129 }
130
131 print $response;
132 print(STDERR logPrefix().">> ".$response."\n") if ($debug);
133}
134
135sub trim
136{
137 my $s = shift;
138 $s =~ s/^\s+//;
139 $s =~ s/\s+$//;
140 return $s;
141}
142
143sub appendError
144{
145 my ($errorArrays) = shift;
146 my($errorName) = shift;
147 my($errorReason) = shift;
148 my($errorCert) = shift;
149 push @$errorArrays, { "error_name" => $errorName, "error_reason" => $errorReason, "error_cert" => $errorCert};
150}
151
152sub createResponse
153{
154 my ($responseErrors) = shift;
155 my $response="";
156 my $i = 0;
157 foreach my $err (@$responseErrors) {
158 $response=$response."error_name_".$i."=".$err->{"error_name"}."\n".
159 "error_reason_".$i."=".$err->{"error_reason"}."\n".
160 "error_cert_".$i."=".$err->{"error_cert"}."\n";
161 $i++;
162 }
163 return $response;
164}
165
166sub parseRequest
167{
168 my($request)=shift;
169 my $hostname = shift;
170 my $errors = shift;
171 my $certs = shift;
172 while ($request !~ /^\s*$/) {
173 $request = trim($request);
174 if ($request =~ /^host=/) {
175 my($vallen) = index($request, "\n");
176 my $host = substr($request, 5, $vallen - 5);
177 $$hostname = $host;
178 $request =~ s/^host=.*$//m;
179 }
180 if ($request =~ /^cert_(\d+)=/) {
181 my $certId = "cert_".$1;
182 my($vallen) = index($request, "-----END CERTIFICATE-----") + length("-----END CERTIFICATE-----");
183 my $x509 = Crypt::OpenSSL::X509->new_from_string(substr($request, index($request, "-----BEGIN")));
184 $certs->{$certId} = $x509;
185 $request = substr($request, $vallen);
186 }
187 elsif ($request =~ /^error_name_(\d+)=(.*)$/m) {
188 my $errorId = $1;
189 my $errorName = $2;
190 $request =~ s/^error_name_\d+=.*$//m;
191 $errors->{$errorId}{"name"} = $errorName;
192 }
193 elsif ($request =~ /^error_cert_(\d+)=(.*)$/m) {
194 my $errorId = $1;
195 my $certId = $2;
196 $request =~ s/^error_cert_\d+=.*$//m;
197 $errors->{$errorId}{"cert"} = $certId;
198 }
199 else {
200 print(STDERR logPrefix()."ParseError on \"".$request."\"\n") if ($debug);
201 $request = "";# finish processing....
202 }
203 }
204}
205
206
207sub logPrefix
208{
209 return strftime("%Y/%m/%d %H:%M:%S.0", localtime)." ".$0." ".$$." | " ;
210}
diff --git a/validatecert.hs b/validatecert.hs
new file mode 100644
index 0000000..b082419
--- /dev/null
+++ b/validatecert.hs
@@ -0,0 +1,222 @@
1{-# LANGUAGE OverloadedStrings, ViewPatterns #-}
2-- validatecert.hs
3--
4-- translation of cert_valid.pl into haskell
5
6import Data.Char
7import Data.Monoid
8import Data.List
9import Data.Maybe
10import qualified Data.Map as Map
11import qualified Data.ByteString.Char8 as S
12import qualified Data.ByteString.Lazy.Char8 as L
13import qualified Data.ByteString.Lazy as L.Word8
14import qualified Codec.Binary.Base64 as Base64
15import Control.Monad
16import System.IO.Error
17import System.IO
18import Data.Map ( Map )
19import Data.Time.LocalTime ( getZonedTime )
20import Data.Time.Format ( formatTime )
21import System.Exit
22import System.Posix.Process ( getProcessID )
23import System.Locale ( defaultTimeLocale )
24import System.Environment ( getProgName, getArgs )
25
26import ScanningParser
27import PEM
28
29continue e body = either (const $ return ()) body e
30
31while f = fixIO (\v -> f (return v))
32
33digits s = S.all isDigit s
34
35bshow :: Show x => x -> S.ByteString
36bshow = S.pack . show
37
38toS = foldl1' (<>) . L.toChunks
39
40
41parseHeader :: S.ByteString -> Either S.ByteString (S.ByteString, S.ByteString, Int, S.ByteString)
42parseHeader first_line = parseHeaderWords $ S.words first_line
43 where
44 parseHeaderWords (channelId:code:bodylen:body:ignored) | not (digits channelId)
45 = Left $ channelId <> " BH message=\"This helper is concurrent and requires\
46 \ the concurrency option to be specified.\"\1"
47 parseHeaderWords (channelId:code:bodylen:body:ignored) | not (digits bodylen)
48 = Left $ channelId <> " BH message=\"cert validator request syntax error.\" \1";
49 parseHeaderWords (channelId:code:bodylen:body:ignored)
50 = Right ( channelId
51 , code
52 , read $ S.unpack bodylen
53 , body <> "\n"
54 )
55 parseHeaderWords (channelId:_)
56 = Left $ channelId <> " BH message=\"Insufficient words in message.\"\1"
57 parseHeaderWords []
58 = Left ""
59
60data ValidationError = ValidationError
61 { veName :: S.ByteString
62 , veCert :: S.ByteString
63 , veReason :: S.ByteString
64 }
65
66type Cert = PEMBlob
67
68certSubject :: Cert -> S.ByteString
69certSubject cert = "TODO:certSubject" -- TODO
70
71certFormatPEM :: Cert -> S.ByteString
72certFormatPEM cert = S.unlines
73 [ "-----BEGIN " <> toS (pemType cert) <> "-----"
74 , S.pack $ intercalate "\n" $ split64s base64
75 , "-----END " <> toS (pemType cert) <> "-----"
76 ]
77 where
78 base64 = Base64.encode $ L.Word8.unpack $ pemBlob cert
79 split64s "" = []
80 split64s dta = line : split64s rest where (line,rest) = splitAt 64 dta
81
82data ValidationRequest = ValidationRequest
83 { vrHostname :: S.ByteString
84 , vrErrors :: Map S.ByteString ValidationError
85 , vrCerts :: Map S.ByteString Cert
86 , vrSyntaxErrors :: [L.ByteString]
87 , vrPeerCertId :: Maybe S.ByteString
88 }
89
90main = do
91 debug <- do
92 args <- getArgs
93 when (not $ null $ ["-h","--help"] `intersect` args) $ do
94 me <- getProgName
95 hPutStr stderr $ usage me
96 [(["-h","--help"], "brief help message")
97 ,(["-d","--debug"], "enable debug messages to stderr")]
98 exitSuccess
99 return $ not $ null $ ["-d","--debug"] `intersect` args
100
101 while $ \next -> do
102 e <- tryIOError S.getLine
103 continue e $ \first_line -> do
104 when (S.all isSpace first_line)
105 next
106 flip (either wlog) (parseHeader first_line) $ \(channelId,code,bodylen,body0) -> do
107 body1 <- L.hGet stdin (bodylen - S.length body0)
108 when debug $ wlog $ "GOT " <> "Code=" <> code <> " " <> bshow bodylen <> "\n"
109 let body = L.fromChunks $ body0 : L.toChunks body1
110 req = parseRequest body
111 when debug $ forM_ (vrSyntaxErrors req) $ \request -> do
112 wlog $ "ParseError on \"" <> toS request <> "\"\n"
113 when debug $ do
114 wlog $ "Parse result:\n"
115 wlog $ "\tFOUND host:" <> vrHostname req <> "\n"
116 let estr = S.intercalate " , " $ map showe $ Map.elems $ vrErrors req
117 showe e = veName e <> "/" <> veCert e
118 wlog $ "\tFOUND ERRORS:" <> estr <> "\n"
119 forM_ (Map.toList $ vrCerts req) $ \(key,cert) -> do
120 wlog $ "\tFOUND cert " <> key <> ": " <> certSubject cert <> "\n"
121 let responseErrors = fmap (\ve -> ve { veReason = "Checked by validatecert.hs" }) $ vrErrors req
122 response0 = createResponse req responseErrors
123 len = bshow $ S.length response0
124 response = if Map.null responseErrors
125 then channelId <> " OK " <> len <> " " <> response <> "\1"
126 else channelId <> " ERR " <> len <> " " <> response <> "\1"
127 S.putStr response
128 hFlush stdout
129 when debug $ wlog $ ">> " <> response <> "\n"
130
131createResponse :: ValidationRequest -> Map S.ByteString ValidationError -> S.ByteString
132createResponse vr responseErrors = S.concat $ zipWith mkresp [0..] $ Map.elems responseErrors
133 where
134 mkresp i err = "error_name_" <> bshow i <> "=" <> veName err <> "\n"
135 <>"error_reason_" <> bshow i <> "=" <> veReason err <> "\n"
136 <>"error_cert_" <> bshow i <> "=" <> certFormatPEM (vrCertFromErr err) <> "\n"
137 vrCertFromErr err = vrCerts vr Map.! veCert err
138
139parseRequest body = parseRequest0 vr0 body
140 where
141 vr0 = ValidationRequest { vrHostname = ""
142 , vrErrors = Map.empty
143 , vrCerts = Map.empty
144 , vrSyntaxErrors = []
145 , vrPeerCertId = Nothing
146 }
147 ve0 = ValidationError { veName = ""
148 , veCert = ""
149 , veReason = ""
150 }
151 parseRequest0 vr request | L.all isSpace request = vr
152
153 parseRequest0 vr (splitEq -> Just ("host",L.break (=='\n')->(hostname,rs)))
154 = parseRequest0 vr' rs
155 where vr' = vr { vrHostname = toS hostname }
156
157 parseRequest0 vr (splitEq -> Just (var,cert)) | "cert_" `L.isPrefixOf` var
158 = parseRequest0 vr' (L.concat rs)
159 where vr' = maybe vr upd mb
160 upd blob = vr { vrCerts = Map.insert (toS var) blob $ vrCerts vr
161 , vrPeerCertId = Just $ fromMaybe (toS var) $ vrPeerCertId vr }
162 p = pemParser (Just "CERTIFICATE")
163 (mb,rs) = scanAndParse1 p $ L.lines cert
164
165 parseRequest0 vr (digitsId . splitEq -> Just (("error_name",d),L.break (=='\n')->(errorName,rs)))
166 = parseRequest0 vr' rs
167 where vr' = vr { vrErrors = Map.alter (setErrorName errorName) (toS d) $ vrErrors vr }
168
169 parseRequest0 vr (digitsId . splitEq -> Just (("error_cert",d),L.break (=='\n')->(certId,rs)))
170 = parseRequest0 vr' rs
171 where vr' = vr { vrErrors = Map.alter (setErrorCert certId) (toS d) $ vrErrors vr }
172
173 parseRequest0 vr req = vr'
174 where
175 vr' = vr { vrSyntaxErrors = syntaxError $ vrSyntaxErrors vr }
176 syntaxError es = es ++ [ req ]
177
178 setErrorName :: L.ByteString -> Maybe ValidationError -> Maybe ValidationError
179 setErrorName x mb = maybe (Just $ ve0 { veName = toS x })
180 (\ve -> Just $ ve { veName = toS x })
181 mb
182
183 setErrorCert :: L.ByteString -> Maybe ValidationError -> Maybe ValidationError
184 setErrorCert x mb = maybe (Just $ ve0 { veCert = toS x })
185 (\ve -> Just $ ve { veCert = toS x })
186 mb
187
188 digitsId mb = do
189 (n,v) <- mb
190 let (n',tl) = L.span isDigit $ L.reverse n
191 if "_" `L.isPrefixOf` tl
192 then Just ( (L.reverse $ L.drop 1 tl, L.reverse n'), v )
193 else Nothing
194
195 splitEq request = if L.null tl then Nothing
196 else Just (hd,L.drop 1 tl)
197 where
198 (hd,tl) = L.break (=='=') $ L.dropWhile isSpace request
199
200wlog msg = do
201 now <- getZonedTime
202 pid <- getProcessID
203 self <- getProgName
204 hPutStr stderr $
205 formatTime defaultTimeLocale "%Y/%m/%d %H:%M:%S.0" now
206 <> " " <> self
207 <> " " <> show pid
208 <> " | " <> S.unpack msg
209
210usage :: String -> [([String],String)] -> String
211usage cmdname argspec = unlines $ intercalate [""] $
212 [ "Usage:"
213 , tab <> cmdname <> " " <> breif argspec
214 ] : map helptext argspec
215 where
216 tab = " "
217 tabbb = tab <> tab <> tab
218 alts as = intercalate " | " as
219 bracket s = "[" <> s <> "]"
220 breif spec = intercalate " " $ map (bracket . alts . fst) spec
221 helptext (as,help) = [ tab <> alts as
222 , tabbb <> help ]