1 -- This file is part of Diohsc
2 -- Copyright (C) 2020 Martin Bays <mbays@sdf.org>
4 -- This program is free software: you can redistribute it and/or modify
5 -- it under the terms of version 3 of the GNU General Public License as
6 -- published by the Free Software Foundation, or any later version.
8 -- You should have received a copy of the GNU General Public License
9 -- along with this program. If not, see http://www.gnu.org/licenses/.
12 {-# LANGUAGE OverloadedStrings #-}
14 module ClientCert
where
16 import Control
.Monad
((<=<))
17 import Control
.Monad
.Trans
(lift
)
18 import Control
.Monad
.Trans
.Maybe (MaybeT
(..), runMaybeT
)
19 import Crypto
.Hash
.Algorithms
(SHA256
(..))
20 import Crypto
.PubKey
.RSA
21 import Data
.ASN1
.BinaryEncoding
(DER
(..))
22 import Data
.ASN1
.Encoding
(decodeASN1
', encodeASN1
')
24 import Data
.ASN1
.Types
(ASN1Object
(..))
25 import Data
.ASN1
.Types
.String (ASN1StringEncoding
(UTF8
))
26 import Data
.Either (fromRight
)
27 import Data
.Foldable
(msum)
32 import System
.FilePath
34 import qualified Crypto
.PubKey
.Ed25519
as Ed25519
35 import qualified Crypto
.PubKey
.RSA
.PKCS15
as PKCS15
36 import qualified Data
.ByteArray
as BA
37 import qualified Data
.ByteString
as BS
38 import qualified Data
.Text
as TS
39 import qualified Data
.Text
.Encoding
as TS
46 import System
.Posix
.Files
49 -- |Certificate chain with secret key for tail cert
50 data ClientCert
= ClientCert CertificateChain PrivKey
53 clientCertFingerprint
:: ClientCert
-> Fingerprint
54 clientCertFingerprint
(ClientCert
(CertificateChain chain
) _
) =
55 fingerprint
$ head chain
57 maybeRight
:: Either a b
-> Maybe b
58 maybeRight
(Left _
) = Nothing
59 maybeRight
(Right b
) = Just b
61 loadClientCert
:: FilePath -> String -> IO (Maybe ClientCert
)
62 loadClientCert path name
=
63 let certpath
= path
</> name
<.> "crt"
64 legacyKeypath
= path
</> name
<.> "rsa"
65 keypath
= path
</> name
<.> "key"
66 in ignoreIOErrAlt
. runMaybeT
$ do
68 (maybeRight
. decodeCertificateChain
. CertificateChainRaw
. map pemContent
69 <=< maybeRight
. pemParseBS
)
70 <$> BS
.readFile certpath
72 [ MaybeT
$ (maybeRight
. (fst <$>) . fromASN1
73 <=< maybeRight
. decodeASN1
' DER
. pemContent
75 <=< maybeRight
. pemParseBS
)
76 <$> ignoreIOErr
(BS
.readFile keypath
)
78 -- Legacy private rsa key format: Show instance of PrivateKey
79 key
<- MaybeT
$ (PrivKeyRSA
<$>) . readMay
<$> ignoreIOErr
(readFile legacyKeypath
)
80 -- Upgrade to standard format
81 lift
. saveClientCert path name
$ ClientCert chain key
84 return $ ClientCert chain key
86 saveClientCert
:: FilePath -> String -> ClientCert
-> IO ()
87 saveClientCert path name
(ClientCert chain key
) =
88 let filepath
= path
</> name
89 certpath
= filepath
<.> "crt"
90 keypath
= filepath
<.> "key"
91 in isSubPath path filepath
>>? ignoreIOErr
$ do
92 let CertificateChainRaw rawCerts
= encodeCertificateChain chain
93 chainPEMs
= map (pemWriteBS
. PEM
"CERTIFICATE" []) rawCerts
94 BS
.writeFile certpath
$ BS
.intercalate
"\n" chainPEMs
95 let header
= case key
of
96 -- Use the header string the openssl commandline tool expects:
97 PrivKeyRSA _
-> "RSA PRIVATE KEY"
99 BS
.writeFile keypath
. pemWriteBS
. PEM header
[] . encodeDER
$ key
101 setFileMode keypath
$ unionFileModes ownerReadMode ownerWriteMode
-- chmod 600
104 encodeDER
:: ASN1Object o
=> o
-> BS
.ByteString
105 encodeDER
= encodeASN1
' DER
. (`toASN1`
[])
107 -- RFC5280: To indicate that a certificate has no well-defined expiration
108 -- date, the notAfter SHOULD be assigned the GeneralizedTime value of
110 notAfterMax
:: DateTime
111 notAfterMax
= DateTime
(Date
9999 December
31) (TimeOfDay
23 59 59 0)
113 -- RFC5280 has no corresponding prescription for notBefore, but
114 -- 19500101000000Z seems the canonical choice.
115 notBeforeMin
:: DateTime
116 notBeforeMin
= DateTime
(Date
1950 January
1) (TimeOfDay
0 0 0 0)
118 data KeyType
= KeyRSA | KeyEd25519
120 generateSelfSigned
:: KeyType
-> String -> IO ClientCert
121 generateSelfSigned tp cn
=
122 let dn
= DistinguishedName
[(getObjectID DnCommonName
,
123 ASN1CharacterString UTF8
. TS
.encodeUtf8
$ TS
.pack cn
)]
125 KeyRSA
-> SignatureALG HashSHA256 PubKeyALG_RSA
126 KeyEd25519
-> SignatureALG_IntrinsicHash PubKeyALG_Ed25519
127 to
= timeConvert notAfterMax
128 from
= timeConvert notBeforeMin
129 cert pubKey
= Certificate
132 , certSignatureAlg
= sigAlg
135 , certValidity
= (from
, to
)
136 , certPubKey
= pubKey
137 , certExtensions
= Extensions Nothing
141 -- generate 2048bit RSA self-signed cert with maximum validity
142 (pubKey
, secKey
) <- generate
256 65537
143 blinder
<- generateBlinder
$ public_n pubKey
144 let signed
= fst $ objectToSignedExact
145 (\b -> (fromRight BS
.empty $
146 PKCS15
.sign
(Just blinder
) (Just SHA256
) secKey b
, sigAlg
, ()))
147 (cert
$ PubKeyRSA pubKey
)
148 return $ ClientCert
(CertificateChain
[signed
]) (PrivKeyRSA secKey
)
150 secKey
<- Ed25519
.generateSecretKey
151 let pubKey
= Ed25519
.toPublic secKey
152 let signed
= fst $ objectToSignedExact
153 (\b -> (BS
.pack
. BA
.unpack
$ Ed25519
.sign secKey pubKey b
, sigAlg
, ()))
154 (cert
$ PubKeyEd25519 pubKey
)
155 return $ ClientCert
(CertificateChain
[signed
]) (PrivKeyEd25519 secKey
)