ca_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. //go:build !windows
  2. // +build !windows
  3. package main
  4. import (
  5. "bytes"
  6. "encoding/pem"
  7. "errors"
  8. "math/bits"
  9. "os"
  10. "strconv"
  11. "strings"
  12. "testing"
  13. "time"
  14. "github.com/slackhq/nebula/cert"
  15. "github.com/stretchr/testify/assert"
  16. "github.com/stretchr/testify/require"
  17. )
  18. func Test_caSummary(t *testing.T) {
  19. assert.Equal(t, "ca <flags>: create a self signed certificate authority", caSummary())
  20. }
  21. func Test_caHelp(t *testing.T) {
  22. var (
  23. defaultArgonMemory string
  24. defaultArgonIterations string
  25. )
  26. if bits.UintSize == 32 {
  27. defaultArgonMemory = strconv.Itoa(64 * 1024)
  28. defaultArgonIterations = strconv.Itoa(3)
  29. } else {
  30. defaultArgonMemory = strconv.Itoa(2 * 1024 * 1024)
  31. defaultArgonIterations = strconv.Itoa(1)
  32. }
  33. ob := &bytes.Buffer{}
  34. caHelp(ob)
  35. assert.Equal(
  36. t,
  37. "Usage of "+os.Args[0]+" ca <flags>: create a self signed certificate authority\n"+
  38. " Pass \"-\" to any path flag to read from stdin or write to stdout.\n"+
  39. " -argon-iterations uint\n"+
  40. " \tOptional: Argon2 iterations parameter used for encrypted private key passphrase (default "+defaultArgonIterations+")\n"+
  41. " -argon-memory uint\n"+
  42. " \tOptional: Argon2 memory parameter (in KiB) used for encrypted private key passphrase (default "+defaultArgonMemory+")\n"+
  43. " -argon-parallelism uint\n"+
  44. " \tOptional: Argon2 parallelism parameter used for encrypted private key passphrase (default 4)\n"+
  45. " -curve string\n"+
  46. " \tEdDSA/ECDSA Curve (25519, P256) (default \"25519\")\n"+
  47. " -duration duration\n"+
  48. " \tOptional: amount of time the certificate should be valid for. Valid time units are seconds: \"s\", minutes: \"m\", hours: \"h\" (default 8760h0m0s)\n"+
  49. " -encrypt\n"+
  50. " \tOptional: prompt for passphrase and write out-key in an encrypted format\n"+
  51. " -groups string\n"+
  52. " \tOptional: comma separated list of groups. This will limit which groups subordinate certs can use\n"+
  53. " -ips string\n"+
  54. " Deprecated, see -networks\n"+
  55. " -name string\n"+
  56. " \tRequired: name of the certificate authority\n"+
  57. " -networks string\n"+
  58. " \tOptional: comma separated list of ip address and network in CIDR notation. This will limit which ip addresses and networks subordinate certs can use in networks\n"+
  59. " -out-crt string\n"+
  60. " \tOptional: path to write the certificate to (default \"ca.crt\")\n"+
  61. " -out-key string\n"+
  62. " \tOptional: path to write the private key to (default \"ca.key\")\n"+
  63. " -out-qr string\n"+
  64. " \tOptional: output a qr code image (png) of the certificate\n"+
  65. optionalPkcs11String(" -pkcs11 string\n \tOptional: PKCS#11 URI to an existing private key\n")+
  66. " -subnets string\n"+
  67. " \tDeprecated, see -unsafe-networks\n"+
  68. " -unsafe-networks string\n"+
  69. " \tOptional: comma separated list of ip address and network in CIDR notation. This will limit which ip addresses and networks subordinate certs can use in unsafe networks\n"+
  70. " -version uint\n"+
  71. " \tOptional: version of the certificate format to use (default 2)\n",
  72. ob.String(),
  73. )
  74. }
  75. func Test_ca(t *testing.T) {
  76. ob := &bytes.Buffer{}
  77. eb := &bytes.Buffer{}
  78. nopw := &StubPasswordReader{
  79. password: []byte(""),
  80. err: nil,
  81. }
  82. errpw := &StubPasswordReader{
  83. password: []byte(""),
  84. err: errors.New("stub error"),
  85. }
  86. passphrase := []byte("DO NOT USE THIS KEY")
  87. testpw := &StubPasswordReader{
  88. password: passphrase,
  89. err: nil,
  90. }
  91. pwPromptEB := "Enter passphrase: "
  92. // required args
  93. assertHelpError(t, ca(
  94. []string{"-version", "1", "-out-key", "nope", "-out-crt", "nope", "duration", "100m"}, ob, eb, nopw,
  95. ), "-name is required")
  96. assert.Empty(t, ob.String())
  97. assert.Empty(t, eb.String())
  98. // ipv4 only ips
  99. assertHelpError(t, ca([]string{"-version", "1", "-name", "ipv6", "-ips", "100::100/100"}, ob, eb, nopw), "invalid -networks definition: v1 certificates can only be ipv4, have 100::100/100")
  100. assert.Empty(t, ob.String())
  101. assert.Empty(t, eb.String())
  102. // ipv4 only subnets
  103. assertHelpError(t, ca([]string{"-version", "1", "-name", "ipv6", "-subnets", "100::100/100"}, ob, eb, nopw), "invalid -unsafe-networks definition: v1 certificates can only be ipv4, have 100::100/100")
  104. assert.Empty(t, ob.String())
  105. assert.Empty(t, eb.String())
  106. // failed key write
  107. ob.Reset()
  108. eb.Reset()
  109. args := []string{"-version", "1", "-name", "test", "-duration", "100m", "-out-crt", "/do/not/write/pleasecrt", "-out-key", "/do/not/write/pleasekey"}
  110. require.EqualError(t, ca(args, ob, eb, nopw), "error while writing out-key: open /do/not/write/pleasekey: "+NoSuchDirError)
  111. assert.Empty(t, ob.String())
  112. assert.Empty(t, eb.String())
  113. // create temp key file
  114. keyF, err := os.CreateTemp("", "test.key")
  115. require.NoError(t, err)
  116. require.NoError(t, os.Remove(keyF.Name()))
  117. // failed cert write
  118. ob.Reset()
  119. eb.Reset()
  120. args = []string{"-version", "1", "-name", "test", "-duration", "100m", "-out-crt", "/do/not/write/pleasecrt", "-out-key", keyF.Name()}
  121. require.EqualError(t, ca(args, ob, eb, nopw), "error while writing out-crt: open /do/not/write/pleasecrt: "+NoSuchDirError)
  122. assert.Empty(t, ob.String())
  123. assert.Empty(t, eb.String())
  124. // create temp cert file
  125. crtF, err := os.CreateTemp("", "test.crt")
  126. require.NoError(t, err)
  127. require.NoError(t, os.Remove(crtF.Name()))
  128. require.NoError(t, os.Remove(keyF.Name()))
  129. // test proper cert with removed empty groups and subnets
  130. ob.Reset()
  131. eb.Reset()
  132. args = []string{"-version", "1", "-name", "test", "-duration", "100m", "-groups", "1,, 2 , ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
  133. require.NoError(t, ca(args, ob, eb, nopw))
  134. assert.Empty(t, ob.String())
  135. assert.Empty(t, eb.String())
  136. // read cert and key files
  137. rb, _ := os.ReadFile(keyF.Name())
  138. lKey, b, c, err := cert.UnmarshalSigningPrivateKeyFromPEM(rb)
  139. assert.Equal(t, cert.Curve_CURVE25519, c)
  140. assert.Empty(t, b)
  141. require.NoError(t, err)
  142. assert.Len(t, lKey, 64)
  143. rb, _ = os.ReadFile(crtF.Name())
  144. lCrt, b, err := cert.UnmarshalCertificateFromPEM(rb)
  145. assert.Empty(t, b)
  146. require.NoError(t, err)
  147. assert.Equal(t, "test", lCrt.Name())
  148. assert.Empty(t, lCrt.Networks())
  149. assert.True(t, lCrt.IsCA())
  150. assert.Equal(t, []string{"1", "2", "3", "4", "5"}, lCrt.Groups())
  151. assert.Empty(t, lCrt.UnsafeNetworks())
  152. assert.Len(t, lCrt.PublicKey(), 32)
  153. assert.Equal(t, time.Duration(time.Minute*100), lCrt.NotAfter().Sub(lCrt.NotBefore()))
  154. assert.Empty(t, lCrt.Issuer())
  155. assert.True(t, lCrt.CheckSignature(lCrt.PublicKey()))
  156. // test encrypted key
  157. os.Remove(keyF.Name())
  158. os.Remove(crtF.Name())
  159. ob.Reset()
  160. eb.Reset()
  161. args = []string{"-version", "1", "-encrypt", "-name", "test", "-duration", "100m", "-groups", "1,2,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
  162. require.NoError(t, ca(args, ob, eb, testpw))
  163. assert.Empty(t, ob.String())
  164. assert.Equal(t, pwPromptEB, eb.String())
  165. // test encrypted key with passphrase environment variable
  166. os.Remove(keyF.Name())
  167. os.Remove(crtF.Name())
  168. ob.Reset()
  169. eb.Reset()
  170. args = []string{"-version", "1", "-encrypt", "-name", "test", "-duration", "100m", "-groups", "1,2,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
  171. os.Setenv("NEBULA_CA_PASSPHRASE", string(passphrase))
  172. require.NoError(t, ca(args, ob, eb, testpw))
  173. assert.Empty(t, eb.String())
  174. os.Setenv("NEBULA_CA_PASSPHRASE", "")
  175. // read encrypted key file and verify default params
  176. rb, _ = os.ReadFile(keyF.Name())
  177. k, _ := pem.Decode(rb)
  178. ned, err := cert.UnmarshalNebulaEncryptedData(k.Bytes)
  179. require.NoError(t, err)
  180. if bits.UintSize == 32 {
  181. assert.Equal(t, uint32(64*1024), ned.EncryptionMetadata.Argon2Parameters.Memory)
  182. assert.Equal(t, uint32(3), ned.EncryptionMetadata.Argon2Parameters.Iterations)
  183. } else {
  184. assert.Equal(t, uint32(2*1024*1024), ned.EncryptionMetadata.Argon2Parameters.Memory)
  185. assert.Equal(t, uint32(1), ned.EncryptionMetadata.Argon2Parameters.Iterations)
  186. }
  187. assert.Equal(t, uint8(4), ned.EncryptionMetadata.Argon2Parameters.Parallelism)
  188. // verify the key is valid and decrypt-able
  189. var curve cert.Curve
  190. curve, lKey, b, err = cert.DecryptAndUnmarshalSigningPrivateKey(passphrase, rb)
  191. assert.Equal(t, cert.Curve_CURVE25519, curve)
  192. require.NoError(t, err)
  193. assert.Empty(t, b)
  194. assert.Len(t, lKey, 64)
  195. // test when reading password results in an error
  196. os.Remove(keyF.Name())
  197. os.Remove(crtF.Name())
  198. ob.Reset()
  199. eb.Reset()
  200. args = []string{"-version", "1", "-encrypt", "-name", "test", "-duration", "100m", "-groups", "1,2,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
  201. require.Error(t, ca(args, ob, eb, errpw))
  202. assert.Empty(t, ob.String())
  203. assert.Equal(t, pwPromptEB, eb.String())
  204. // test when user fails to enter a password
  205. os.Remove(keyF.Name())
  206. os.Remove(crtF.Name())
  207. ob.Reset()
  208. eb.Reset()
  209. args = []string{"-version", "1", "-encrypt", "-name", "test", "-duration", "100m", "-groups", "1,2,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
  210. require.EqualError(t, ca(args, ob, eb, nopw), "no passphrase specified, remove -encrypt flag to write out-key in plaintext")
  211. assert.Empty(t, ob.String())
  212. assert.Equal(t, strings.Repeat(pwPromptEB, 5), eb.String()) // prompts 5 times before giving up
  213. // create valid cert/key for overwrite tests
  214. os.Remove(keyF.Name())
  215. os.Remove(crtF.Name())
  216. ob.Reset()
  217. eb.Reset()
  218. args = []string{"-version", "1", "-name", "test", "-duration", "100m", "-groups", "1,, 2 , ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
  219. require.NoError(t, ca(args, ob, eb, nopw))
  220. // test that we won't overwrite existing certificate file
  221. ob.Reset()
  222. eb.Reset()
  223. args = []string{"-version", "1", "-name", "test", "-duration", "100m", "-groups", "1,, 2 , ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
  224. require.EqualError(t, ca(args, ob, eb, nopw), "refusing to overwrite existing CA key: "+keyF.Name())
  225. assert.Empty(t, ob.String())
  226. assert.Empty(t, eb.String())
  227. // test that we won't overwrite existing key file
  228. os.Remove(keyF.Name())
  229. ob.Reset()
  230. eb.Reset()
  231. args = []string{"-version", "1", "-name", "test", "-duration", "100m", "-groups", "1,, 2 , ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
  232. require.EqualError(t, ca(args, ob, eb, nopw), "refusing to overwrite existing CA cert: "+crtF.Name())
  233. assert.Empty(t, ob.String())
  234. assert.Empty(t, eb.String())
  235. os.Remove(keyF.Name())
  236. }
  237. func Test_ca_stdio(t *testing.T) {
  238. nopw := &StubPasswordReader{}
  239. keyF, err := os.CreateTemp("", "ca.key")
  240. require.NoError(t, err)
  241. os.Remove(keyF.Name())
  242. defer os.Remove(keyF.Name())
  243. crtF, err := os.CreateTemp("", "ca.crt")
  244. require.NoError(t, err)
  245. os.Remove(crtF.Name())
  246. defer os.Remove(crtF.Name())
  247. // out-crt on stdout, out-key on disk
  248. ob := &bytes.Buffer{}
  249. eb := &bytes.Buffer{}
  250. require.NoError(t, ca([]string{"-name", "test-ca", "-duration", "1h", "-out-crt", "-", "-out-key", keyF.Name()}, ob, eb, nopw))
  251. assert.Empty(t, eb.String())
  252. c, _, err := cert.UnmarshalCertificateFromPEM(ob.Bytes())
  253. require.NoError(t, err)
  254. assert.True(t, c.IsCA())
  255. assert.Equal(t, "test-ca", c.Name())
  256. // out-key on stdout, out-crt on disk
  257. os.Remove(keyF.Name())
  258. ob.Reset()
  259. eb.Reset()
  260. require.NoError(t, ca([]string{"-name", "test-ca", "-duration", "1h", "-out-crt", crtF.Name(), "-out-key", "-"}, ob, eb, nopw))
  261. assert.Empty(t, eb.String())
  262. _, _, curve, err := cert.UnmarshalSigningPrivateKeyFromPEM(ob.Bytes())
  263. require.NoError(t, err)
  264. assert.Equal(t, cert.Curve_CURVE25519, curve)
  265. // dual stdout is rejected up front
  266. os.Remove(crtF.Name())
  267. ob.Reset()
  268. eb.Reset()
  269. require.EqualError(t,
  270. ca([]string{"-name", "test-ca", "-duration", "1h", "-out-crt", "-", "-out-key", "-"}, ob, eb, nopw),
  271. `-out-key and -out-crt both set to "-", only one output may write to stdout`)
  272. assert.Empty(t, ob.String())
  273. // an output conflict combined with -encrypt must error BEFORE prompting
  274. // for a passphrase; pr would record any read attempt
  275. tracker := &trackingPasswordReader{}
  276. ob.Reset()
  277. eb.Reset()
  278. require.EqualError(t,
  279. ca([]string{"-name", "test-ca", "-duration", "1h", "-encrypt", "-out-crt", "-", "-out-key", "-"}, ob, eb, tracker),
  280. `-out-key and -out-crt both set to "-", only one output may write to stdout`)
  281. assert.Empty(t, ob.String())
  282. assert.Empty(t, eb.String())
  283. assert.Zero(t, tracker.calls, "passphrase prompt should not have been called")
  284. }
  285. type trackingPasswordReader struct {
  286. calls int
  287. }
  288. func (pr *trackingPasswordReader) ReadPassword() ([]byte, error) {
  289. pr.calls++
  290. return []byte(""), nil
  291. }