diff options
-rw-r--r-- | ScanningParser.hs | 11 | ||||
-rwxr-xr-x | cert_valid.pl | 210 | ||||
-rw-r--r-- | validatecert.hs | 222 |
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 @@ | |||
3 | module ScanningParser | 3 | module ScanningParser |
4 | ( ScanningParser(..) | 4 | ( ScanningParser(..) |
5 | , scanAndParse | 5 | , scanAndParse |
6 | , scanAndParse1 | ||
6 | ) where | 7 | ) where |
7 | 8 | ||
8 | import Data.Maybe | 9 | import 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 | ||
65 | scanAndParse1 :: ScanningParser a c -> [a] -> (Maybe c, [a]) | ||
66 | scanAndParse1 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 | |||
7 | use warnings; | ||
8 | use strict; | ||
9 | use Getopt::Long; | ||
10 | use Pod::Usage; | ||
11 | use Crypt::OpenSSL::X509; | ||
12 | use FileHandle; | ||
13 | use POSIX qw(strftime); | ||
14 | |||
15 | my $debug = 0; | ||
16 | my $help = 0; | ||
17 | |||
18 | =pod | ||
19 | |||
20 | =head1 NAME | ||
21 | |||
22 | cert_valid.pl - A fake cert validation helper for Squid | ||
23 | |||
24 | =head1 SYNOPSIS | ||
25 | |||
26 | cert_valid.pl [-d | --debug] [-h | --help] | ||
27 | |||
28 | =over 8 | ||
29 | |||
30 | =item B<-h | --help> | ||
31 | |||
32 | brief help message | ||
33 | |||
34 | =item B<-d | --debug> | ||
35 | |||
36 | enable debug messages to stderr | ||
37 | |||
38 | =back | ||
39 | |||
40 | =head1 DESCRIPTION | ||
41 | |||
42 | Retrieves 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 | |||
48 | This program is free software. You may redistribute copies of it under the | ||
49 | terms of the GNU General Public License version 2, or (at your opinion) any | ||
50 | later version. | ||
51 | |||
52 | =cut | ||
53 | |||
54 | GetOptions( | ||
55 | 'help' => \$help, | ||
56 | 'debug' => \$debug, | ||
57 | ) or pod2usage(1); | ||
58 | |||
59 | pod2usage(1) if ($help); | ||
60 | |||
61 | $|=1; | ||
62 | while (<>) { | ||
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 | |||
135 | sub trim | ||
136 | { | ||
137 | my $s = shift; | ||
138 | $s =~ s/^\s+//; | ||
139 | $s =~ s/\s+$//; | ||
140 | return $s; | ||
141 | } | ||
142 | |||
143 | sub 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 | |||
152 | sub 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 | |||
166 | sub 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 | |||
207 | sub 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 | |||
6 | import Data.Char | ||
7 | import Data.Monoid | ||
8 | import Data.List | ||
9 | import Data.Maybe | ||
10 | import qualified Data.Map as Map | ||
11 | import qualified Data.ByteString.Char8 as S | ||
12 | import qualified Data.ByteString.Lazy.Char8 as L | ||
13 | import qualified Data.ByteString.Lazy as L.Word8 | ||
14 | import qualified Codec.Binary.Base64 as Base64 | ||
15 | import Control.Monad | ||
16 | import System.IO.Error | ||
17 | import System.IO | ||
18 | import Data.Map ( Map ) | ||
19 | import Data.Time.LocalTime ( getZonedTime ) | ||
20 | import Data.Time.Format ( formatTime ) | ||
21 | import System.Exit | ||
22 | import System.Posix.Process ( getProcessID ) | ||
23 | import System.Locale ( defaultTimeLocale ) | ||
24 | import System.Environment ( getProgName, getArgs ) | ||
25 | |||
26 | import ScanningParser | ||
27 | import PEM | ||
28 | |||
29 | continue e body = either (const $ return ()) body e | ||
30 | |||
31 | while f = fixIO (\v -> f (return v)) | ||
32 | |||
33 | digits s = S.all isDigit s | ||
34 | |||
35 | bshow :: Show x => x -> S.ByteString | ||
36 | bshow = S.pack . show | ||
37 | |||
38 | toS = foldl1' (<>) . L.toChunks | ||
39 | |||
40 | |||
41 | parseHeader :: S.ByteString -> Either S.ByteString (S.ByteString, S.ByteString, Int, S.ByteString) | ||
42 | parseHeader 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 | |||
60 | data ValidationError = ValidationError | ||
61 | { veName :: S.ByteString | ||
62 | , veCert :: S.ByteString | ||
63 | , veReason :: S.ByteString | ||
64 | } | ||
65 | |||
66 | type Cert = PEMBlob | ||
67 | |||
68 | certSubject :: Cert -> S.ByteString | ||
69 | certSubject cert = "TODO:certSubject" -- TODO | ||
70 | |||
71 | certFormatPEM :: Cert -> S.ByteString | ||
72 | certFormatPEM 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 | |||
82 | data 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 | |||
90 | main = 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 | |||
131 | createResponse :: ValidationRequest -> Map S.ByteString ValidationError -> S.ByteString | ||
132 | createResponse 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 | |||
139 | parseRequest 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 | |||
200 | wlog 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 | |||
210 | usage :: String -> [([String],String)] -> String | ||
211 | usage 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 ] | ||