1 -- This file is part of htalkat
2 -- Copyright (C) 2021 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/.
11 {-# LANGUAGE LambdaCase #-}
12 {-# LANGUAGE OverloadedStrings #-}
16 import Network
.TLS
(Credential
, credentialLoadX509
)
17 import System
.FilePath ((<.>), (</>))
19 import Data
.ASN1
.BinaryEncoding
(DER
(..))
20 import Data
.ASN1
.Encoding
(encodeASN1
')
22 import Data
.ASN1
.Types
(ASN1Object
(..))
23 import Data
.ASN1
.Types
.String (ASN1StringEncoding
(UTF8
))
29 import System
.Posix
.Files
(ownerReadMode
, ownerWriteMode
,
30 setFileMode
, unionFileModes
)
33 import qualified Crypto
.PubKey
.Ed25519
as Ed25519
34 import qualified Data
.ByteArray
as BA
35 import qualified Data
.ByteString
as BS
36 import qualified Data
.Text
as TS
37 import qualified Data
.Text
.Encoding
as TS
48 #if !(MIN_VERSION_base
(4,11,0))
52 data IdentityType
= IdConnect | IdListen
deriving Eq
54 loadIdentity
:: FilePath -> IdentityType
-> IO (Maybe Credential
)
55 loadIdentity ddir tp
= do
56 let base
= ddir
</> "id"
57 certpath
= base
<> (if tp
== IdConnect
then "-connect" else "-listen") <.> "crt"
58 keypath
= base
<.> "key"
59 ignoreIOErrAlt
$ eitherToMaybe
<$> credentialLoadX509 certpath keypath
61 saveIdentity
:: FilePath -> PrivKey
-> CertificateChain
-> CertificateChain
-> IO ()
62 saveIdentity ddir secKey connectChain listenChain
= do
63 let base
= ddir
</> "id"
64 connectCertpath
= base
<> "-connect" <.> "crt"
65 listenCertpath
= base
<> "-listen" <.> "crt"
66 keypath
= base
<.> "key"
68 writeChain connectCertpath connectChain
69 writeChain listenCertpath listenChain
70 BS
.writeFile keypath
. pemWriteBS
. PEM
"PRIVATE KEY" [] . encodeDER
$ secKey
72 setFileMode keypath
$ unionFileModes ownerReadMode ownerWriteMode
-- chmod 600
75 writeChain certpath chain
=
76 let CertificateChainRaw rawCerts
= encodeCertificateChain chain
77 chainPEMs
= map (pemWriteBS
. PEM
"CERTIFICATE" []) rawCerts
78 in BS
.writeFile certpath
$ BS
.intercalate
"\n" chainPEMs
79 encodeDER
:: ASN1Object o
=> o
-> BS
.ByteString
80 encodeDER
= encodeASN1
' DER
. (`toASN1`
[])
82 createOrShowIdentity
:: FilePath -> Maybe String -> IO ()
83 createOrShowIdentity ddir mCN
=
84 mapM (loadIdentity ddir
) [IdConnect
,IdListen
] >>= \case
85 [Just
(connectChain
,_
), Just
(_
,_
)]
86 | Just cert
<- takeTailCert connectChain
87 , Nothing
<- mCN
-> do
89 [ "Your fingerprint: talkat:" <> showFingerprint
(spkiFingerprint cert
)
90 , "Your public name: " <> certCN cert
92 [Just
(_
,key
), Just
(listenChain
,_
)]
95 PrivKeyEd25519 secKey
-> do
96 connectChain
' <- generateSelfSigned secKey cn
97 saveIdentity ddir key connectChain
' listenChain
98 _
-> putStrLn "Error: Can't regenerate non-ED25519 key!"
100 putStrLn "Generating new identity."
101 secKey
<- Ed25519
.generateSecretKey
103 putStrLn "Enter a public name for this identity (can be blank)."
104 putStrLn "This will be shown to anyone you connect to, but not to incoming callers."
105 putStrLn "(You can reset this name later with 'htalkat i NEW_NAME')"
106 promptLine
"Public name: "
107 cn
<- maybe promptCN pure mCN
108 connectChain
<- generateSelfSigned secKey cn
109 listenChain
<- generateSelfSigned secKey
""
110 saveIdentity ddir
(PrivKeyEd25519 secKey
) connectChain listenChain
111 let Just cert
= takeTailCert connectChain
112 fp
= spkiFingerprint cert
113 putStrLn $ "Your fingerprint: talkat:" <> showFingerprint fp
114 writeName ddir
(User fp
(parseHost
"localhost")) $ Named
"self"
116 generateSelfSigned
:: Ed25519
.SecretKey
-> String -> IO CertificateChain
117 generateSelfSigned secKey cn
=
118 let dn
= DistinguishedName
[(getObjectID DnCommonName
,
119 ASN1CharacterString UTF8
. TS
.encodeUtf8
$ TS
.pack cn
)]
120 sigAlg
= SignatureALG_IntrinsicHash PubKeyALG_Ed25519
121 -- RFC5280: To indicate that a certificate has no well-defined expiration
122 -- date, the notAfter SHOULD be assigned the GeneralizedTime value of
124 notAfterMax
:: DateTime
125 notAfterMax
= DateTime
(Date
9999 December
31) (TimeOfDay
23 59 59 0)
127 -- RFC5280 has no corresponding prescription for notBefore.
128 -- 19500101000000Z seems the canonical choice, but it seems to get
129 -- loaded as 2050 for some reason. So we use 1970.
130 notBeforeMin
:: DateTime
131 notBeforeMin
= DateTime
(Date
1970 January
1) (TimeOfDay
0 0 0 0)
133 cert pubKey
= X
.Certificate
136 , certSignatureAlg
= sigAlg
139 , certValidity
= (timeConvert notBeforeMin
, timeConvert notAfterMax
)
140 , certPubKey
= pubKey
141 , certExtensions
= Extensions Nothing
144 let pubKey
= Ed25519
.toPublic secKey
145 let signed
= fst $ objectToSignedExact
146 (\b -> (BS
.pack
. BA
.unpack
$ Ed25519
.sign secKey pubKey b
, sigAlg
, ()))
147 (cert
$ PubKeyEd25519 pubKey
)
148 pure
$ CertificateChain
[signed
]