forked from TrueCloudLab/certificates
Compare commits
753 commits
herman/acm
...
tcl/master
Author | SHA1 | Date | |
---|---|---|---|
b13ba41053 | |||
f4dfed3bf3 | |||
|
a1350b14fb | ||
|
c9df65ebae | ||
|
d9d7c52997 | ||
|
ff424fa944 | ||
|
7282245e88 | ||
|
9a7582d1d3 | ||
|
7796ad8f90 | ||
|
2d666cfc4f | ||
|
904f416d20 | ||
|
d89c3a942e | ||
|
aa30c2c73c | ||
|
31533c4a15 | ||
|
5bfe96d8c7 | ||
|
d604a900ed | ||
|
0c3a1aea38 | ||
|
cbc46d11e5 | ||
|
1755c8d60f | ||
|
f7da9a6f30 | ||
|
f7c33d0878 | ||
|
7bca0c2349 | ||
|
90bac46a00 | ||
|
9edf43b188 | ||
|
f998b19bb3 | ||
|
41ff437a6b | ||
|
d1607e460d | ||
|
b9a3031b84 | ||
|
4059184b43 | ||
|
0607027412 | ||
|
d39a28535d | ||
|
a6c6df9283 | ||
|
a6dd12675c | ||
|
e1db416c7d | ||
|
2b3bf88001 | ||
|
df20d8fcb4 | ||
|
49d1ca0a49 | ||
|
0723987573 | ||
|
ef0cd093e3 | ||
|
10b7280e17 | ||
|
a5801b3c74 | ||
|
18bc0f333b | ||
|
44f3b97e61 | ||
|
4c70abcd62 | ||
|
9a60734504 | ||
|
dc03edbc27 | ||
|
9cb2c4365d | ||
|
03011660a8 | ||
|
701a0ea3f4 | ||
|
62212d23bf | ||
|
eae423ed14 | ||
|
d59b16cb2a | ||
|
f7f66ad3ed | ||
|
898bd6a0f4 | ||
|
4f21d0c256 | ||
|
e5c46d4264 | ||
|
4ce88e579e | ||
|
48855080ff | ||
|
b029359e78 | ||
|
a577ee9699 | ||
|
fb7b299110 | ||
|
ce89c09031 | ||
|
e38e632dca | ||
|
f8b318bb90 | ||
|
73cb04318a | ||
|
b2b8b48949 | ||
|
de52aee9b1 | ||
|
5e68a6d49a | ||
|
1df9419212 | ||
|
f7d1376327 | ||
|
6c4825b149 | ||
|
648e60ac1a | ||
|
6aa00b3c89 | ||
|
81228b481f | ||
|
f4cee836bf | ||
|
83fc176a87 | ||
|
c74bab5161 | ||
|
d78c9f831b | ||
|
05d6f81ee9 | ||
|
eeb912e025 | ||
|
67d32685c7 | ||
|
56a2a17ff8 | ||
|
a3301bf65b | ||
|
e400294238 | ||
|
c02d4adcca | ||
|
02d4657ee6 | ||
|
4bd89a8e5f | ||
|
d97b254a1d | ||
|
f3555ee0e7 | ||
|
3d29316d0b | ||
|
7ac0e4d21d | ||
|
53c7774d3c | ||
|
825c5dd754 | ||
|
ccb248394b | ||
|
9d7dff6995 | ||
|
7731edd816 | ||
|
cbbc54e980 | ||
|
e5bd90918d | ||
|
20c05ed592 | ||
|
1fe8b70190 | ||
|
561328de05 | ||
|
8e22402190 | ||
|
ff40459035 | ||
|
f38ea204d1 | ||
|
5d069f6b94 | ||
|
bee6bf7069 | ||
|
c1be2761bb | ||
|
b4d532fd1f | ||
|
ef839a5932 | ||
|
6a9241fb8e | ||
|
96c8cd812a | ||
|
71fcdf8a0a | ||
|
fffeeec74e | ||
|
1f0a2a8443 | ||
|
ce4fd3d514 | ||
|
1031324273 | ||
|
101120b977 | ||
|
a477e41915 | ||
|
ed9a1275cd | ||
|
cdf55a410b | ||
|
19896dc04b | ||
|
9efc47ae42 | ||
|
4f7cdfbaa2 | ||
|
ffb6d1c0f2 | ||
|
b4d536db57 | ||
|
57865b399b | ||
|
96dcab88ac | ||
|
5e05d6ec2e | ||
|
ca6b7049e5 | ||
|
2ef45a204f | ||
|
36cb5bf1d4 | ||
|
f06d22a138 | ||
|
6db3f5f093 | ||
|
40b69ea95c | ||
|
e71b62e95c | ||
|
a49ee2c03d | ||
|
b96831ee45 | ||
|
df13351586 | ||
|
4a60f8f71f | ||
|
7f54153a1b | ||
|
e52e79f745 | ||
|
8abb511f64 | ||
|
8b256f0351 | ||
|
0b832e389d | ||
|
2b209b94e8 | ||
|
0c2b00f6a1 | ||
|
3c7b247712 | ||
|
017c3273ef | ||
|
f93548df40 | ||
|
5735d1d354 | ||
|
6bf7943a1b | ||
|
f4c6a72967 | ||
|
93426d72a9 | ||
|
570b10b8e8 | ||
|
f17bfdf57d | ||
|
4c56877d97 | ||
|
1180e33228 | ||
|
afd5d46a90 | ||
|
2139121683 | ||
|
81140f859c | ||
|
39e658b527 | ||
|
922f702da3 | ||
|
ef951f2075 | ||
|
8c53dc9029 | ||
|
0153ff4377 | ||
|
f9ec62f46c | ||
|
eba93da6d6 | ||
|
d797941137 | ||
|
5e35aca29c | ||
|
60a4512abe | ||
|
cb1dc8055d | ||
|
c73f157ea4 | ||
|
4f7a4f63f7 | ||
|
4bb88adf63 | ||
|
e8c1e8719d | ||
|
19d72c9905 | ||
|
3a2e60a139 | ||
|
5ea72a2432 | ||
|
eec7d1ee6a | ||
|
047bb6a826 | ||
|
d19c77795e | ||
|
3a50a2fa28 | ||
|
bb33134f8a | ||
|
668ff9b515 | ||
|
5f0f0f4bcc | ||
|
ad4d8e6c68 | ||
|
419478d1e5 | ||
|
27cdcaf5ee | ||
|
05f7ab979f | ||
|
1420c762e0 | ||
|
26afd6c932 | ||
|
6bc2164ea1 | ||
|
91f51252c5 | ||
|
a56b112216 | ||
|
f2fda93cad | ||
|
4dedbf7678 | ||
|
74414e530b | ||
|
a784038025 | ||
|
9e198b0e4a | ||
|
631b773257 | ||
|
b59a8f0a9f | ||
|
0099ec7244 | ||
|
0c49d119d5 | ||
|
a7480ebe4f | ||
|
c2f2c7176c | ||
|
7ad81a6c54 | ||
|
99f9b2fb3e | ||
|
574351a8f7 | ||
|
ef337f5285 | ||
|
b5dbeefcc6 | ||
|
5ec9e761ca | ||
|
3665616015 | ||
|
848e44e5c8 | ||
|
b034c06ac8 | ||
|
38c715ca46 | ||
|
4133e7d069 | ||
|
2e1e529731 | ||
|
f3bd1d3dbd | ||
|
432dd7ce10 | ||
|
aeb02e280e | ||
|
64e39cb0c9 | ||
|
cfd65484fc | ||
|
d9aa2c110f | ||
|
a815039283 | ||
|
b5baa55a60 | ||
|
ed1a62206e | ||
|
1c38e252a6 | ||
|
e25acff13c | ||
|
dfc56f21b8 | ||
|
9cd4b362f7 | ||
|
b4da554aa6 | ||
|
6905979537 | ||
|
827fcb6a06 | ||
|
b6957358fc | ||
|
023491bcf2 | ||
|
ac35f3489c | ||
|
09bd7705cd | ||
|
f88ef6621f | ||
|
79cd42527e | ||
|
52023d6083 | ||
|
390acab7d0 | ||
|
57a704f008 | ||
|
21f14e5708 | ||
|
ae30f6e96b | ||
|
1cc3ad27a5 | ||
|
bf53b394a1 | ||
|
720cafb69c | ||
|
a3018d9db5 | ||
|
ca108564ff | ||
|
0cb5acd01c | ||
|
8ed523ea67 | ||
|
09cbe8ba65 | ||
|
094f0521e2 | ||
|
f91a31f9b6 | ||
|
df2909e712 | ||
|
25e35aa0ad | ||
|
f874e31fff | ||
|
b92f37a61d | ||
|
2b76d11631 | ||
|
897f4711df | ||
|
1b1df26864 | ||
|
92e25f0f7f | ||
|
1859ed2666 | ||
|
1420f441d5 | ||
|
bdd4d0004e | ||
|
d8a2839955 | ||
|
e6339a3761 | ||
|
6e0644beb2 | ||
|
334bc81694 | ||
|
7ad1ecf518 | ||
|
7a3989e7f2 | ||
|
7b26ef72a0 | ||
|
942f8bfc9f | ||
|
589a62df74 | ||
|
213b31bc2c | ||
|
e1c7e8f00b | ||
|
6297bace1a | ||
|
69489480ab | ||
|
b3a0769778 | ||
|
6588efdb01 | ||
|
19a91671a6 | ||
|
745c1cc130 | ||
|
c72826a690 | ||
|
124b1e6273 | ||
|
bb3cddd6f1 | ||
|
5943c3955e | ||
|
8747156bcc | ||
|
442f2fe5f9 | ||
|
b2c2eec76b | ||
|
4378300c80 | ||
|
b8ee206f71 | ||
|
201be0891f | ||
|
79b3924322 | ||
|
dd43e9e09f | ||
|
152a0a2f3e | ||
|
4fb00940c6 | ||
|
6452afc45c | ||
|
7c54154013 | ||
|
702f844fa2 | ||
|
4d6ecf9a48 | ||
|
12d8ca526a | ||
|
2eb90bf45e | ||
|
7700bb77da | ||
|
831a1e35ea | ||
|
f8adb0a51c | ||
|
fe63f3e832 | ||
|
060a2f186c | ||
|
4fd9a9b92b | ||
|
23423814d3 | ||
|
0d5c40e059 | ||
|
176cf30a6f | ||
|
59462e826c | ||
|
10958a124b | ||
|
b02c43cf8e | ||
|
81b1d2ede6 | ||
|
e0b9f3960c | ||
|
b4f8100c72 | ||
|
ae0be0acbd | ||
|
5f835dc808 | ||
|
6915feaae9 | ||
|
7c1c32d86b | ||
|
8e47f05dba | ||
|
790139d5a7 | ||
|
ca9f8dc576 | ||
|
cfcc95de93 | ||
|
96c6613739 | ||
|
effe729d53 | ||
|
bb068f8280 | ||
|
d59d6c414f | ||
|
25599f8ad5 | ||
|
2f2e3dea0f | ||
|
5fbee3d3ef | ||
|
0d80473157 | ||
|
12d905be3e | ||
|
045ae52452 | ||
|
c2c246b062 | ||
|
ff7b8830fe | ||
|
74e6245e90 | ||
|
5ff0dde819 | ||
|
da95c44943 | ||
|
6ba20209c2 | ||
|
ec3be2359a | ||
|
ebe7e5d019 | ||
|
03cb74a449 | ||
|
c9814be699 | ||
|
2c57415657 | ||
|
3c76834807 | ||
|
6be15819d6 | ||
|
da00046a61 | ||
|
2cef8d10ee | ||
|
067f9c9a5f | ||
|
3a6fc5e0b4 | ||
|
0f1c509e4b | ||
|
b76028f3ba | ||
|
1c59b3f132 | ||
|
50b4011b03 | ||
|
40538d8224 | ||
|
4b7fa2524d | ||
|
0df942b8f6 | ||
|
972bfb9689 | ||
|
e741c60afb | ||
|
dd9b97221e | ||
|
ed4af06a56 | ||
|
197b79bb48 | ||
|
0c5e7f1b5c | ||
|
d79e1343ae | ||
|
9a539f22fc | ||
|
c32e84b436 | ||
|
0f9128c873 | ||
|
2ab9beb7ed | ||
|
3b1be62663 | ||
|
7c632629dd | ||
|
ed61c5df5f | ||
|
60a9e41c1c | ||
|
edee01c80c | ||
|
1c38113e44 | ||
|
4bb25d4a52 | ||
|
39f46d31b9 | ||
|
f1724ea8c5 | ||
|
fc452e560c | ||
|
4621b95f38 | ||
|
2d174472e7 | ||
|
3a6e90498c | ||
|
cb8a2ee69f | ||
|
626a3a87b4 | ||
|
925a228656 | ||
|
07fd03c3f3 | ||
|
64d9ad7b38 | ||
|
5bab65aa49 | ||
|
925f32e82f | ||
|
466fe8280e | ||
|
fb39fccf6a | ||
|
29deb4befa | ||
|
98cb439b41 | ||
|
cc6b87d1a4 | ||
|
2ab9483952 | ||
|
e4073270f3 | ||
|
627506b519 | ||
|
fd921e5b26 | ||
|
0b26698e72 | ||
|
bab77f257a | ||
|
a78ddc7cc5 | ||
|
2e86a392a8 | ||
|
2cd5708103 | ||
|
ad8a95cc10 | ||
|
dc8b196823 | ||
|
328276eaeb | ||
|
ad5cbd9a0e | ||
|
a017238874 | ||
|
313bf2354b | ||
|
640bd0b7c7 | ||
|
c836c7ab40 | ||
|
8242895909 | ||
|
844cfd3bad | ||
|
ac4d5e63ab | ||
|
985a0e4858 | ||
|
762ce06d84 | ||
|
34dc119cf7 | ||
|
9cc35d1505 | ||
|
e7a4a1f43c | ||
|
8ba1b44cd8 | ||
|
a063961175 | ||
|
dae0ba9008 | ||
|
32f4908310 | ||
|
c5c07be298 | ||
|
b13b527d18 | ||
|
b5961beba9 | ||
|
319333f936 | ||
|
85f6554c5e | ||
|
001c156b28 | ||
|
407496234f | ||
|
27a50d50d3 | ||
|
75ffbae5a7 | ||
|
a4e64665da | ||
|
5d87201abc | ||
|
9007e2ef75 | ||
|
3fb38a3c14 | ||
|
c2d441fbfd | ||
|
e07734d90d | ||
|
260f40c6bc | ||
|
f26e70cc16 | ||
|
c365d8580e | ||
|
f2e1c56c6c | ||
|
47dad19bbc | ||
|
4e3a6e67f1 | ||
|
a0423a4539 | ||
|
c6e34f7b84 | ||
|
5cce76672d | ||
|
002a058807 | ||
|
be4cd17b40 | ||
|
262814ac43 | ||
|
b655fcda21 | ||
|
596be4bec7 | ||
|
afc81d96d8 | ||
|
27a1ab640d | ||
|
b8c306ebfa | ||
|
36da484604 | ||
|
9197de3e96 | ||
|
b7f4881972 | ||
|
27bbc3682b | ||
|
362be72120 | ||
|
8a2e49a1e3 | ||
|
51503dabac | ||
|
b9b60d50fe | ||
|
f63a01a4de | ||
|
7a8c6c0abe | ||
|
6d9c184e5a | ||
|
d6f9b3336d | ||
|
c79d4e9316 | ||
|
adad7ef970 | ||
|
fcfd2b9bdc | ||
|
1f19b8ec5e | ||
|
ffc30f49b1 | ||
|
a800ffe447 | ||
|
47bd5a80d9 | ||
|
fa8d0a68c4 | ||
|
893147d23a | ||
|
817edcbba5 | ||
|
ddd5057f63 | ||
|
e0215e7243 | ||
|
ca6f4514fd | ||
|
07c56f577a | ||
|
3a89428b0f | ||
|
b31cf1fc18 | ||
|
2b928b1afd | ||
|
85cd9a1277 | ||
|
4cf25ede24 | ||
|
c169defc73 | ||
|
dde9330244 | ||
|
75ac5d3889 | ||
|
3ef73fa66b | ||
|
80cbcb652b | ||
|
57c1c2071d | ||
|
e8726d24fa | ||
|
202b17c3f2 | ||
|
ae684a557a | ||
|
7354e6e905 | ||
|
920c4f02c5 | ||
|
e351bd90dc | ||
|
a7b2f5f27d | ||
|
8f7fae585c | ||
|
1c4aa6ad79 | ||
|
d4e81723ee | ||
|
55a684fe5a | ||
|
4fcfc9481b | ||
|
656b9ab217 | ||
|
9cbee4da33 | ||
|
88febefbcf | ||
|
2891f6b397 | ||
|
3eae04928f | ||
|
02d679e160 | ||
|
c7f226bcec | ||
|
068a2dae8e | ||
|
e00781873e | ||
|
bae9a0c152 | ||
|
6c0cb23125 | ||
|
e27c6c529b | ||
|
9d90d0cef3 | ||
|
3728cee02a | ||
|
be8c0b4531 | ||
|
4ccc9a0c32 | ||
|
6136dbb196 | ||
|
bd577e7531 | ||
|
e53a4b2ed5 | ||
|
917d8dc103 | ||
|
b85b52d7b5 | ||
|
ea3f2fee7b | ||
|
9d9236c985 | ||
|
d26414a864 | ||
|
22d2c1c31f | ||
|
4e077f997e | ||
|
995b6d1b6c | ||
|
c36b36f070 | ||
|
3e0b603eb4 | ||
|
2d582e5694 | ||
|
89c8c6d0a0 | ||
|
f066ac3d40 | ||
|
51c7f56030 | ||
|
6d4fd7d016 | ||
|
812fee7630 | ||
|
59775fff0c | ||
|
8200d19894 | ||
|
c43d59a69a | ||
|
0af15a0538 | ||
|
a9359522e6 | ||
|
a718359b7f | ||
|
2e39b6305e | ||
|
b9f238ad4d | ||
|
aed1738ad0 | ||
|
c407354c70 | ||
|
25340c2bf6 | ||
|
3e96113162 | ||
|
016973fd2b | ||
|
e90fe4bfa0 | ||
|
9d04e7d1dc | ||
|
54c560f620 | ||
|
fd38dd34f9 | ||
|
c9793561ff | ||
|
49718f1bbb | ||
|
70da534893 | ||
|
398213af51 | ||
|
caf0628b8c | ||
|
aefdfc7be7 | ||
|
18555a3cb2 | ||
|
53f2ecdad9 | ||
|
d07c9accea | ||
|
7b45968198 | ||
|
91775f6d67 | ||
|
361e2b2907 | ||
|
c103458ee9 | ||
|
e436c36f8b | ||
|
b83f268b4d | ||
|
f410ef6628 | ||
|
c3f6dcf7e7 | ||
|
f7d153efa7 | ||
|
70828b882f | ||
|
a7db13d47b | ||
|
9ee11fd850 | ||
|
3676c59599 | ||
|
cbc0864370 | ||
|
d981b9e0dc | ||
|
57001168a5 | ||
|
c423e2f664 | ||
|
459bfc4c4f | ||
|
3262ffd43b | ||
|
1a5523f5c0 | ||
|
da5d2b405c | ||
|
65b5a636df | ||
|
7203739369 | ||
|
6516384160 | ||
|
e0994bed9d | ||
|
a7e597450a | ||
|
317efa4568 | ||
|
8616d3160f | ||
|
674206320c | ||
|
b5837f20c9 | ||
|
c9ee4a9f9d | ||
|
7a78c76199 | ||
|
b142fc70f7 | ||
|
32edc54946 | ||
|
8598ff6a1c | ||
|
b27d36d556 | ||
|
2522efe27a | ||
|
2ee4218a69 | ||
|
90cdcdd2eb | ||
|
aa4e5bed76 | ||
|
764b7bb02f | ||
|
f7df865687 | ||
|
a8f9b07aae | ||
|
bd1938b0da | ||
|
cebb7d7ef0 | ||
|
939e60b378 | ||
|
139e115f6f | ||
|
c66218330a | ||
|
a258ea3e2d | ||
|
66858a3870 | ||
|
72b6cb6d31 | ||
|
97cc09e26c | ||
|
4c687efb17 | ||
|
818dffe6fe | ||
|
21666ba887 | ||
|
8538ff06b7 | ||
|
37e80964ae | ||
|
c071907cd1 | ||
|
505c411a67 | ||
|
5f130895f3 | ||
|
a4f6b1ba0f | ||
|
87079a87d6 | ||
|
3abb42dd1d | ||
|
b1c396c829 | ||
|
eba73d2aea | ||
|
32feef3275 | ||
|
f2628697db | ||
|
2ef5c98384 | ||
|
5f0fa57844 | ||
|
211b1c01c0 | ||
|
510122f026 | ||
|
9b02b03293 | ||
|
2c5e41b093 | ||
|
70abbdfc70 | ||
|
8139179084 | ||
|
af4183df18 | ||
|
e5c5b0cb49 | ||
|
d0e81af524 | ||
|
ef75d4f3c6 | ||
|
3faa8717b3 | ||
|
0027f93fb9 | ||
|
7101fbb0ee | ||
|
132b32b5a5 | ||
|
7420172d63 | ||
|
6fe0fc852a | ||
|
906c5067b9 | ||
|
acdf080308 | ||
|
9d4bef8cc9 | ||
|
de73a3e8de | ||
|
bfe29def59 | ||
|
1f4443d858 | ||
|
a3c7e0b15b | ||
|
d0bba915f2 | ||
|
fa7c55a0ec | ||
|
ea229e2ba8 | ||
|
2fcadce977 | ||
|
630e7fbba9 | ||
|
8e2fc8ea5a | ||
|
7d46516bbf | ||
|
68f571645b | ||
|
fa4986d215 | ||
|
2b19ba41fb | ||
|
c9e7af3722 | ||
|
0bedd22850 | ||
|
8374c0d26e | ||
|
965d59c0a8 | ||
|
7830c5bd72 | ||
|
2eba5326db | ||
|
ccd93684c3 | ||
|
bc838c3af2 | ||
|
246566a195 | ||
|
b94c0d09be | ||
|
0214e015a0 | ||
|
23045e1812 | ||
|
4c7a2ce3eb | ||
|
15dc7901e5 | ||
|
d68c765e20 | ||
|
72e2c4eb2e | ||
|
4b79405dac | ||
|
a6e85cbbf6 | ||
|
325d8bca4f | ||
|
d46c5b2f40 | ||
|
aa3ffea07c | ||
|
2d4efc8292 | ||
|
75bb196193 | ||
|
a8125846dd | ||
|
120629edab | ||
|
c26d2c8328 | ||
|
89bccf7796 | ||
|
ffff9af323 | ||
|
7c5e5b2b87 | ||
|
f3d1863ec6 | ||
|
6af9437875 | ||
|
18a648cffa | ||
|
1e0ea6f958 | ||
|
33458c88aa | ||
|
99299faeeb | ||
|
f0a24bd8ca | ||
|
567d96c771 | ||
|
191d9e8629 | ||
|
debe565e42 | ||
|
2b3b2c283a | ||
|
89b6aa924a | ||
|
5f5315260a | ||
|
de5b0ef5c2 | ||
|
248da10f32 | ||
|
6f2b4d3042 | ||
|
f3d2bd7a19 | ||
|
746ee2b6db | ||
|
a795f4281c | ||
|
25cbe02b9e | ||
|
2de7d3fcf0 | ||
|
ab0d2503ae | ||
|
3f58f30b21 | ||
|
75bff055fc | ||
|
2959aa676d | ||
|
8b54e25f64 | ||
|
2a15e3eee1 | ||
|
45e594f98c | ||
|
7bea2f4d0e | ||
|
495494ce8f | ||
|
6686f0437d | ||
|
0263468424 | ||
|
40baf73dff | ||
|
4e19aa4c52 | ||
|
221e756f40 | ||
|
0829f37fe8 | ||
|
4a4f7ca9ba | ||
|
924082bb49 | ||
|
d2483f3a70 | ||
|
9fa5f46213 | ||
|
60671b07d7 | ||
|
c8b38c0e13 | ||
|
773741eda8 | ||
|
49c41636cc | ||
|
53dbe2309b | ||
|
a607ab189a | ||
|
d417ce3232 | ||
|
668cb6f39c | ||
|
7d024cc4cb | ||
|
e8fdb703c9 | ||
|
8520c861d5 | ||
|
45975b061c | ||
|
222b52db13 | ||
|
26cb52a573 | ||
|
8545adea92 | ||
|
56926b9012 |
267 changed files with 16817 additions and 9087 deletions
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
4
.github/labeler.yml
vendored
4
.github/labeler.yml
vendored
|
@ -1,4 +0,0 @@
|
|||
needs triage:
|
||||
- '**' # index.php | src/main.php
|
||||
- '.*' # .gitignore
|
||||
- '.*/**' # .github/workflows/label.yml
|
27
.github/workflows/ci.yml
vendored
Normal file
27
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- 'v*'
|
||||
branches:
|
||||
- "master"
|
||||
pull_request:
|
||||
workflow_call:
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
uses: smallstep/workflows/.github/workflows/goCI.yml@main
|
||||
with:
|
||||
only-latest-golang: false
|
||||
os-dependencies: 'libpcsclite-dev'
|
||||
run-codeql: true
|
||||
test-command: 'V=1 make test'
|
||||
secrets: inherit
|
9
.github/workflows/code-scan-cron.yml
vendored
Normal file
9
.github/workflows/code-scan-cron.yml
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
code-scan:
|
||||
uses: smallstep/workflows/.github/workflows/code-scan.yml@main
|
||||
secrets:
|
||||
GITLEAKS_LICENSE_KEY: ${{ secrets.GITLEAKS_LICENSE_KEY }}
|
72
.github/workflows/codeql-analysis.yml
vendored
72
.github/workflows/codeql-analysis.yml
vendored
|
@ -1,72 +0,0 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '30 3 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
22
.github/workflows/dependabot-auto-merge.yml
vendored
Normal file
22
.github/workflows/dependabot-auto-merge.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
name: Dependabot auto-merge
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v1.1.1
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Enable auto-merge for Dependabot PRs
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
182
.github/workflows/release.yml
vendored
182
.github/workflows/release.yml
vendored
|
@ -7,63 +7,43 @@ on:
|
|||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Lint, Test, Build
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
go: [ '1.18', '1.19' ]
|
||||
outputs:
|
||||
is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
-
|
||||
name: Install Deps
|
||||
id: install-deps
|
||||
run: sudo apt-get -y install libpcsclite-dev
|
||||
-
|
||||
name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: ${{ secrets.GOLANGCI_LINT_VERSION }}
|
||||
args: --timeout=30m
|
||||
-
|
||||
name: Test, Build
|
||||
id: lint_test_build
|
||||
run: V=1 make ci
|
||||
ci:
|
||||
uses: smallstep/certificates/.github/workflows/ci.yml@master
|
||||
secrets: inherit
|
||||
|
||||
create_release:
|
||||
name: Create Release
|
||||
needs: test
|
||||
runs-on: ubuntu-20.04
|
||||
needs: ci
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_IMAGE: smallstep/step-ca
|
||||
outputs:
|
||||
debversion: ${{ steps.extract-tag.outputs.DEB_VERSION }}
|
||||
version: ${{ steps.extract-tag.outputs.VERSION }}
|
||||
is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
|
||||
docker_tags: ${{ env.DOCKER_TAGS }}
|
||||
docker_tags_hsm: ${{ env.DOCKER_TAGS_HSM }}
|
||||
steps:
|
||||
-
|
||||
name: Extract Tag Names
|
||||
id: extract-tag
|
||||
run: |
|
||||
DEB_VERSION=$(echo ${GITHUB_REF#refs/tags/v} | sed 's/-/./')
|
||||
echo "::set-output name=DEB_VERSION::${DEB_VERSION}"
|
||||
-
|
||||
name: Is Pre-release
|
||||
- name: Is Pre-release
|
||||
id: is_prerelease
|
||||
run: |
|
||||
set +e
|
||||
echo ${{ github.ref }} | grep "\-rc.*"
|
||||
OUT=$?
|
||||
if [ $OUT -eq 0 ]; then IS_PRERELEASE=true; else IS_PRERELEASE=false; fi
|
||||
echo "::set-output name=IS_PRERELEASE::${IS_PRERELEASE}"
|
||||
-
|
||||
name: Create Release
|
||||
echo "IS_PRERELEASE=${IS_PRERELEASE}" >> ${GITHUB_OUTPUT}
|
||||
- name: Extract Tag Names
|
||||
id: extract-tag
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT}
|
||||
echo "DOCKER_TAGS=${{ env.DOCKER_IMAGE }}:${VERSION}" >> ${GITHUB_ENV}
|
||||
echo "DOCKER_TAGS_HSM=${{ env.DOCKER_IMAGE }}:${VERSION}-hsm" >> ${GITHUB_ENV}
|
||||
- name: Add Latest Tag
|
||||
if: steps.is_prerelease.outputs.IS_PRERELEASE == 'false'
|
||||
run: |
|
||||
echo "DOCKER_TAGS=${{ env.DOCKER_TAGS }},${{ env.DOCKER_IMAGE }}:latest" >> ${GITHUB_ENV}
|
||||
echo "DOCKER_TAGS_HSM=${{ env.DOCKER_TAGS_HSM }},${{ env.DOCKER_IMAGE }}:hsm" >> ${GITHUB_ENV}
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
|
@ -75,89 +55,37 @@ jobs:
|
|||
prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
|
||||
|
||||
goreleaser:
|
||||
name: Upload Assets To Github w/ goreleaser
|
||||
runs-on: ubuntu-20.04
|
||||
needs: create_release
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
-
|
||||
name: APT Install
|
||||
id: aptInstall
|
||||
run: sudo apt-get -y install build-essential debhelper fakeroot
|
||||
-
|
||||
name: Build Debian package
|
||||
id: make_debian
|
||||
run: |
|
||||
PATH=$PATH:/usr/local/go/bin:/home/admin/go/bin
|
||||
make debian
|
||||
# need to restore the git state otherwise goreleaser fails due to dirty state
|
||||
git restore debian/changelog
|
||||
git clean -fd
|
||||
-
|
||||
name: Install cosign
|
||||
uses: sigstore/cosign-installer@v1.1.0
|
||||
with:
|
||||
cosign-release: 'v1.1.0'
|
||||
-
|
||||
name: Write cosign key to disk
|
||||
id: write_key
|
||||
run: echo "${{ secrets.COSIGN_KEY }}" > "/tmp/cosign.key"
|
||||
-
|
||||
name: Get Release Date
|
||||
id: release_date
|
||||
run: |
|
||||
RELEASE_DATE=$(date +"%y-%m-%d")
|
||||
echo "::set-output name=RELEASE_DATE::${RELEASE_DATE}"
|
||||
-
|
||||
name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@5a54d7e660bda43b405e8463261b3d25631ffe86 # v2.7.0
|
||||
with:
|
||||
version: 'v1.7.0'
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
|
||||
DEB_VERSION: ${{ needs.create_release.outputs.debversion }}
|
||||
RELEASE_DATE: ${{ steps.release_date.outputs.RELEASE_DATE }}
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
uses: smallstep/workflows/.github/workflows/goreleaser.yml@main
|
||||
secrets: inherit
|
||||
|
||||
build_upload_docker:
|
||||
name: Build & Upload Docker Images
|
||||
runs-on: ubuntu-20.04
|
||||
needs: test
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.19'
|
||||
-
|
||||
name: Install cosign
|
||||
uses: sigstore/cosign-installer@v1.1.0
|
||||
with:
|
||||
cosign-release: 'v1.1.0'
|
||||
-
|
||||
name: Write cosign key to disk
|
||||
id: write_key
|
||||
run: echo "${{ secrets.COSIGN_KEY }}" > "/tmp/cosign.key"
|
||||
-
|
||||
name: Build
|
||||
id: build
|
||||
run: |
|
||||
PATH=$PATH:/usr/local/go/bin:/home/admin/go/bin
|
||||
make docker-artifacts
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
|
||||
needs: create_release
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main
|
||||
with:
|
||||
platforms: linux/amd64,linux/386,linux/arm,linux/arm64
|
||||
tags: ${{ needs.create_release.outputs.docker_tags }}
|
||||
docker_image: smallstep/step-ca
|
||||
docker_file: docker/Dockerfile
|
||||
secrets: inherit
|
||||
|
||||
build_upload_docker_hsm:
|
||||
name: Build & Upload HSM Enabled Docker Images
|
||||
needs: create_release
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main
|
||||
with:
|
||||
platforms: linux/amd64,linux/386,linux/arm,linux/arm64
|
||||
tags: ${{ needs.create_release.outputs.docker_tags_hsm }}
|
||||
docker_image: smallstep/step-ca
|
||||
docker_file: docker/Dockerfile.hsm
|
||||
secrets: inherit
|
||||
|
|
49
.github/workflows/test.yml
vendored
49
.github/workflows/test.yml
vendored
|
@ -1,49 +0,0 @@
|
|||
name: Lint, Test, Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- 'v*'
|
||||
branches:
|
||||
- "**"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lintTestBuild:
|
||||
name: Lint, Test, Build
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
go: [ '1.18', '1.19' ]
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
-
|
||||
name: Install Deps
|
||||
id: install-deps
|
||||
run: sudo apt-get -y install libpcsclite-dev
|
||||
-
|
||||
name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: ${{ secrets.GOLANGCI_LINT_VERSION }}
|
||||
args: --timeout=30m
|
||||
-
|
||||
name: Test, Build
|
||||
id: lint_test_build
|
||||
run: V=1 make ci
|
||||
-
|
||||
name: Codecov
|
||||
if: matrix.go == '1.19'
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage.out # optional
|
||||
name: codecov-umbrella # optional
|
||||
fail_ci_if_error: true # optional (default = false)
|
23
.github/workflows/triage.yml
vendored
23
.github/workflows/triage.yml
vendored
|
@ -4,26 +4,13 @@ on:
|
|||
issues:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
|
||||
jobs:
|
||||
|
||||
label:
|
||||
name: Label PR
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request_target'
|
||||
steps:
|
||||
- uses: actions/labeler@v3.0.2
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
add-to-project:
|
||||
name: Add to Triage Project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.3.0
|
||||
with:
|
||||
project-url: https://github.com/orgs/smallstep/projects/94
|
||||
github-token: ${{ secrets.TRIAGE_PAT }}
|
||||
triage:
|
||||
uses: smallstep/workflows/.github/workflows/triage.yml@main
|
||||
secrets: inherit
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -24,3 +24,4 @@ output
|
|||
vendor
|
||||
.idea
|
||||
.envrc
|
||||
.vscode
|
||||
|
|
18
.gitleaksignore
Normal file
18
.gitleaksignore
Normal file
|
@ -0,0 +1,18 @@
|
|||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:85
|
||||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:107
|
||||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:108
|
||||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:129
|
||||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:131
|
||||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:136
|
||||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:138
|
||||
7c9ab9814fb676cb3c125c3dac4893271f1b7ae5:README.md:generic-api-key:282
|
||||
fb7140444ac8f1fa1245a80e49d17e206f7435f3:docs/provisioners.md:generic-api-key:110
|
||||
e4de7f07e82118b3f926716666b620db058fa9f7:docs/revocation.md:generic-api-key:73
|
||||
e4de7f07e82118b3f926716666b620db058fa9f7:docs/revocation.md:generic-api-key:113
|
||||
e4de7f07e82118b3f926716666b620db058fa9f7:docs/revocation.md:generic-api-key:151
|
||||
8b2de42e9cf6ce99f53a5049881e1d6077d5d66e:docs/docker.md:generic-api-key:152
|
||||
3939e855264117e81531df777a642ea953d325a7:autocert/init/ca/intermediate_ca_key:private-key:1
|
||||
e72f08703753facfa05f2d8c68f9f6a3745824b8:README.md:generic-api-key:244
|
||||
e70a5dae7de0b6ca40a0393c09c28872d4cfa071:autocert/README.md:generic-api-key:365
|
||||
e70a5dae7de0b6ca40a0393c09c28872d4cfa071:autocert/README.md:generic-api-key:366
|
||||
c284a2c0ab1c571a46443104be38c873ef0c7c6d:config.json:generic-api-key:10
|
|
@ -1,74 +0,0 @@
|
|||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
settings:
|
||||
printf:
|
||||
funcs:
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
|
||||
revive:
|
||||
min-confidence: 0
|
||||
gocyclo:
|
||||
min-complexity: 10
|
||||
maligned:
|
||||
suggest-new: true
|
||||
dupl:
|
||||
threshold: 100
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
depguard:
|
||||
list-type: blacklist
|
||||
packages:
|
||||
# logging is allowed only by logutils.Log, logrus
|
||||
# is allowed to use only in logutils package
|
||||
- github.com/sirupsen/logrus
|
||||
misspell:
|
||||
locale: US
|
||||
lll:
|
||||
line-length: 140
|
||||
goimports:
|
||||
local-prefixes: github.com/golangci/golangci-lint
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
- diagnostic
|
||||
disabled-checks:
|
||||
- commentFormatting
|
||||
- commentedOutCode
|
||||
- evalOrder
|
||||
- hugeParam
|
||||
- octalLiteral
|
||||
- rangeValCopy
|
||||
- tooManyResultsChecker
|
||||
- unnamedResult
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- gocritic
|
||||
- gofmt
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- revive
|
||||
- staticcheck
|
||||
- unused
|
||||
|
||||
run:
|
||||
skip-dirs:
|
||||
- pkg
|
||||
|
||||
issues:
|
||||
exclude:
|
||||
- can't lint
|
||||
- declaration of "err" shadows declaration at line
|
||||
- should have a package comment, unless it's in another file for this package
|
||||
- error strings should not be capitalized or end with punctuation or a newline
|
||||
- Wrapf call needs 1 arg but has 2 args
|
||||
- cs.NegotiatedProtocolIsMutual is deprecated
|
168
.goreleaser.yml
168
.goreleaser.yml
|
@ -26,58 +26,17 @@ builds:
|
|||
flags:
|
||||
- -trimpath
|
||||
main: ./cmd/step-ca/main.go
|
||||
binary: bin/step-ca
|
||||
ldflags:
|
||||
- -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}}
|
||||
-
|
||||
id: step-cloudkms-init
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
- darwin_amd64
|
||||
- darwin_arm64
|
||||
- freebsd_amd64
|
||||
- linux_386
|
||||
- linux_amd64
|
||||
- linux_arm64
|
||||
- linux_arm_5
|
||||
- linux_arm_6
|
||||
- linux_arm_7
|
||||
- windows_amd64
|
||||
flags:
|
||||
- -trimpath
|
||||
main: ./cmd/step-cloudkms-init/main.go
|
||||
binary: bin/step-cloudkms-init
|
||||
ldflags:
|
||||
- -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}}
|
||||
-
|
||||
id: step-awskms-init
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
- darwin_amd64
|
||||
- darwin_arm64
|
||||
- freebsd_amd64
|
||||
- linux_386
|
||||
- linux_amd64
|
||||
- linux_arm64
|
||||
- linux_arm_5
|
||||
- linux_arm_6
|
||||
- linux_arm_7
|
||||
- windows_amd64
|
||||
flags:
|
||||
- -trimpath
|
||||
main: ./cmd/step-awskms-init/main.go
|
||||
binary: bin/step-awskms-init
|
||||
binary: step-ca
|
||||
ldflags:
|
||||
- -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}}
|
||||
|
||||
archives:
|
||||
-
|
||||
- &ARCHIVE
|
||||
# Can be used to change the archive formats for specific GOOSs.
|
||||
# Most common use case is to archive as zip on Windows.
|
||||
# Default is empty.
|
||||
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
|
||||
rlcp: true
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
@ -85,9 +44,51 @@ archives:
|
|||
files:
|
||||
- README.md
|
||||
- LICENSE
|
||||
allow_different_binary_count: true
|
||||
-
|
||||
<< : *ARCHIVE
|
||||
id: unversioned
|
||||
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
|
||||
|
||||
|
||||
nfpms:
|
||||
# Configure nFPM for .deb and .rpm releases
|
||||
#
|
||||
# See https://nfpm.goreleaser.com/configuration/
|
||||
# and https://goreleaser.com/customization/nfpm/
|
||||
#
|
||||
# Useful tools for debugging .debs:
|
||||
# List file contents: dpkg -c dist/step_...deb
|
||||
# Package metadata: dpkg --info dist/step_....deb
|
||||
#
|
||||
- &NFPM
|
||||
builds:
|
||||
- step-ca
|
||||
package_name: step-ca
|
||||
file_name_template: "{{ .PackageName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
|
||||
vendor: Smallstep Labs
|
||||
homepage: https://github.com/smallstep/certificates
|
||||
maintainer: Smallstep <techadmin@smallstep.com>
|
||||
description: >
|
||||
step-ca is an online certificate authority for secure, automated certificate management.
|
||||
license: Apache 2.0
|
||||
section: utils
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
priority: optional
|
||||
bindir: /usr/bin
|
||||
contents:
|
||||
- src: debian/copyright
|
||||
dst: /usr/share/doc/step-ca/copyright
|
||||
-
|
||||
<< : *NFPM
|
||||
id: unversioned
|
||||
file_name_template: "{{ .PackageName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
|
||||
|
||||
source:
|
||||
enabled: true
|
||||
rlcp: true
|
||||
name_template: '{{ .ProjectName }}_{{ .Version }}'
|
||||
|
||||
checksum:
|
||||
|
@ -97,8 +98,9 @@ checksum:
|
|||
|
||||
signs:
|
||||
- cmd: cosign
|
||||
stdin: '{{ .Env.COSIGN_PWD }}'
|
||||
args: ["sign-blob", "-key=/tmp/cosign.key", "-output=${signature}", "${artifact}"]
|
||||
signature: "${artifact}.sig"
|
||||
certificate: "${artifact}.pem"
|
||||
args: ["sign-blob", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}"]
|
||||
artifacts: all
|
||||
|
||||
snapshot:
|
||||
|
@ -139,17 +141,17 @@ release:
|
|||
|
||||
#### Linux
|
||||
|
||||
- 📦 [step-ca_linux_{{ .Version }}_amd64.tar.gz](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_linux_{{ .Version }}_amd64.tar.gz)
|
||||
- 📦 [step-ca_{{ .Env.DEB_VERSION }}_amd64.deb](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_{{ .Env.DEB_VERSION }}_amd64.deb)
|
||||
- 📦 [step-ca_linux_{{ .Version }}_amd64.tar.gz](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_linux_{{ .Version }}_amd64.tar.gz)
|
||||
- 📦 [step-ca_{{ .Version }}_amd64.deb](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_{{ .Version }}_amd64.deb)
|
||||
|
||||
#### OSX Darwin
|
||||
|
||||
- 📦 [step-ca_darwin_{{ .Version }}_amd64.tar.gz](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_amd64.tar.gz)
|
||||
- 📦 [step-ca_darwin_{{ .Version }}_arm64.tar.gz](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_arm64.tar.gz)
|
||||
- 📦 [step-ca_darwin_{{ .Version }}_amd64.tar.gz](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_amd64.tar.gz)
|
||||
- 📦 [step-ca_darwin_{{ .Version }}_arm64.tar.gz](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_arm64.tar.gz)
|
||||
|
||||
#### Windows
|
||||
|
||||
- 📦 [step-ca_windows_{{ .Version }}_arm64.zip](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_windows_{{ .Version }}_amd64.zip)
|
||||
- 📦 [step-ca_windows_{{ .Version }}_amd64.zip](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_windows_{{ .Version }}_amd64.zip)
|
||||
|
||||
For more builds across platforms and architectures, see the `Assets` section below.
|
||||
And for packaged versions (Docker, k8s, Homebrew), see our [installation docs](https://smallstep.com/docs/step-ca/installation).
|
||||
|
@ -164,8 +166,10 @@ release:
|
|||
|
||||
```
|
||||
cosign verify-blob \
|
||||
-key https://raw.githubusercontent.com/smallstep/certificates/master/cosign.pub \
|
||||
-signature ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig
|
||||
--certificate ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig.pem \
|
||||
--signature ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig \
|
||||
--certificate-identity-regexp "https://github\.com/smallstep/certificates/.*" \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz
|
||||
```
|
||||
|
||||
|
@ -195,38 +199,40 @@ release:
|
|||
# - glob: ./glob/**/to/**/file/**/*
|
||||
# - glob: ./glob/foo/to/bar/file/foobar/override_from_previous
|
||||
|
||||
scoop:
|
||||
# Template for the url which is determined by the given Token (github or gitlab)
|
||||
# Default for github is "https://github.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
|
||||
# Default for gitlab is "https://gitlab.com/<repo_owner>/<repo_name>/uploads/{{ .ArtifactUploadHash }}/{{ .ArtifactName }}"
|
||||
# Default for gitea is "https://gitea.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
|
||||
url_template: "http://github.com/smallstep/certificates/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
|
||||
scoops:
|
||||
-
|
||||
ids: [ default ]
|
||||
# Template for the url which is determined by the given Token (github or gitlab)
|
||||
# Default for github is "https://github.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
|
||||
# Default for gitlab is "https://gitlab.com/<repo_owner>/<repo_name>/uploads/{{ .ArtifactUploadHash }}/{{ .ArtifactName }}"
|
||||
# Default for gitea is "https://gitea.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
|
||||
url_template: "http://github.com/smallstep/certificates/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
|
||||
# Repository to push the app manifest to.
|
||||
bucket:
|
||||
owner: smallstep
|
||||
name: scoop-bucket
|
||||
|
||||
# Repository to push the app manifest to.
|
||||
bucket:
|
||||
owner: smallstep
|
||||
name: scoop-bucket
|
||||
# Git author used to commit to the repository.
|
||||
# Defaults are shown.
|
||||
commit_author:
|
||||
name: goreleaserbot
|
||||
email: goreleaser@smallstep.com
|
||||
|
||||
# Git author used to commit to the repository.
|
||||
# Defaults are shown.
|
||||
commit_author:
|
||||
name: goreleaserbot
|
||||
email: goreleaser@smallstep.com
|
||||
# The project name and current git tag are used in the format string.
|
||||
commit_msg_template: "Scoop update for {{ .ProjectName }} version {{ .Tag }}"
|
||||
|
||||
# The project name and current git tag are used in the format string.
|
||||
commit_msg_template: "Scoop update for {{ .ProjectName }} version {{ .Tag }}"
|
||||
# Your app's homepage.
|
||||
# Default is empty.
|
||||
homepage: "https://smallstep.com/docs/step-ca"
|
||||
|
||||
# Your app's homepage.
|
||||
# Default is empty.
|
||||
homepage: "https://smallstep.com/docs/step-ca"
|
||||
# Skip uploads for prerelease.
|
||||
skip_upload: auto
|
||||
|
||||
# Skip uploads for prerelease.
|
||||
skip_upload: auto
|
||||
# Your app's description.
|
||||
# Default is empty.
|
||||
description: "A private certificate authority (X.509 & SSH) & ACME server for secure automated certificate management, so you can use TLS everywhere & SSO for SSH."
|
||||
|
||||
# Your app's description.
|
||||
# Default is empty.
|
||||
description: "A private certificate authority (X.509 & SSH) & ACME server for secure automated certificate management, so you can use TLS everywhere & SSO for SSH."
|
||||
# Your app's license
|
||||
# Default is empty.
|
||||
license: "Apache-2.0"
|
||||
|
||||
# Your app's license
|
||||
# Default is empty.
|
||||
license: "Apache-2.0"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
#!/usr/bin/env sh
|
||||
read -r firstline < .VERSION
|
||||
last_half="${firstline##*tag: }"
|
||||
if [[ ${last_half::1} == "v" ]]; then
|
||||
|
|
235
CHANGELOG.md
235
CHANGELOG.md
|
@ -1,33 +1,200 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### TEMPLATE -- do not alter or remove
|
||||
## TEMPLATE -- do not alter or remove
|
||||
|
||||
---
|
||||
|
||||
## [x.y.z] - aaaa-bb-cc
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
|
||||
### Security
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved authentication for ACME requests using kid and provisioner name
|
||||
(smallstep/certificates#1386).
|
||||
|
||||
|
||||
## [v0.24.2] - 2023-05-11
|
||||
|
||||
### Added
|
||||
- Added support for ACME device-attest-01 challenge.
|
||||
|
||||
- Log SSH certificates (smallstep/certificates#1374)
|
||||
- CRL endpoints on the HTTP server (smallstep/certificates#1372)
|
||||
- Dynamic SCEP challenge validation using webhooks (smallstep/certificates#1366)
|
||||
- For Docker deployments, added DOCKER_STEPCA_INIT_PASSWORD_FILE. Useful for pointing to a Docker Secret in the container (smallstep/certificates#1384)
|
||||
|
||||
### Changed
|
||||
|
||||
- Depend on [smallstep/go-attestation](https://github.com/smallstep/go-attestation) instead of [google/go-attestation](https://github.com/google/go-attestation)
|
||||
- Render CRLs into http.ResponseWriter instead of memory (smallstep/certificates#1373)
|
||||
- Redaction of SCEP static challenge when listing provisioners (smallstep/certificates#1204)
|
||||
|
||||
### Fixed
|
||||
|
||||
- VaultCAS certificate lifetime (smallstep/certificates#1376)
|
||||
|
||||
## [v0.24.1] - 2023-04-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Docker image name for HSM support (smallstep/certificates#1348)
|
||||
|
||||
## [v0.24.0] - 2023-04-12
|
||||
|
||||
### Added
|
||||
|
||||
- Add ACME `device-attest-01` support with TPM 2.0
|
||||
(smallstep/certificates#1063).
|
||||
- Add support for new Azure SDK, sovereign clouds, and HSM keys on Azure KMS
|
||||
(smallstep/crypto#192, smallstep/crypto#197, smallstep/crypto#198,
|
||||
smallstep/certificates#1323, smallstep/certificates#1309).
|
||||
- Add support for ASN.1 functions on certificate templates
|
||||
(smallstep/crypto#208, smallstep/certificates#1345)
|
||||
- Add `DOCKER_STEPCA_INIT_ADDRESS` to configure the address to use in a docker
|
||||
container (smallstep/certificates#1262).
|
||||
- Make sure that the CSR used matches the attested key when using AME
|
||||
`device-attest-01` challenge (smallstep/certificates#1265).
|
||||
- Add support for compacting the Badger DB (smallstep/certificates#1298).
|
||||
- Build and release cleanups (smallstep/certificates#1322,
|
||||
smallstep/certificates#1329, smallstep/certificates#1340).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix support for PKCS #7 RSA-OAEP decryption through
|
||||
[smallstep/pkcs7#4](https://github.com/smallstep/pkcs7/pull/4), as used in
|
||||
SCEP.
|
||||
- Fix RA installation using `scripts/install-step-ra.sh`
|
||||
(smallstep/certificates#1255).
|
||||
- Clarify error messages on policy errors (smallstep/certificates#1287,
|
||||
smallstep/certificates#1278).
|
||||
- Clarify error message on OIDC email validation (smallstep/certificates#1290).
|
||||
- Mark the IDP critical in the generated CRL data (smallstep/certificates#1293).
|
||||
- Disable database if CA is initialized with the `--no-db` flag
|
||||
(smallstep/certificates#1294).
|
||||
|
||||
## [v0.23.2] - 2023-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- Added [`step-kms-plugin`](https://github.com/smallstep/step-kms-plugin) to
|
||||
docker images, and a new image, `smallstep/step-ca-hsm`, compiled with cgo
|
||||
(smallstep/certificates#1243).
|
||||
- Added [`scoop`](https://scoop.sh) packages back to the release
|
||||
(smallstep/certificates#1250).
|
||||
- Added optional flag `--pidfile` which allows passing a filename where step-ca
|
||||
will write its process id (smallstep/certificates#1251).
|
||||
- Added helpful message on CA startup when config can't be opened
|
||||
(smallstep/certificates#1252).
|
||||
- Improved validation and error messages on `device-attest-01` orders
|
||||
(smallstep/certificates#1235).
|
||||
|
||||
### Removed
|
||||
|
||||
- The deprecated CLI utils `step-awskms-init`, `step-cloudkms-init`,
|
||||
`step-pkcs11-init`, `step-yubikey-init` have been removed.
|
||||
[`step`](https://github.com/smallstep/cli) and
|
||||
[`step-kms-plugin`](https://github.com/smallstep/step-kms-plugin) should be
|
||||
used instead (smallstep/certificates#1240).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed remote management flags in docker images (smallstep/certificates#1228).
|
||||
|
||||
## [v0.23.1] - 2023-01-10
|
||||
|
||||
### Added
|
||||
|
||||
- Added configuration property `.crl.idpURL` to be able to set a custom Issuing
|
||||
Distribution Point in the CRL (smallstep/certificates#1178).
|
||||
- Added WithContext methods to the CA client (smallstep/certificates#1211).
|
||||
- Docker: Added environment variables for enabling Remote Management and ACME
|
||||
provisioner (smallstep/certificates#1201).
|
||||
- Docker: The entrypoint script now generates and displays an initial JWK
|
||||
provisioner password by default when the CA is being initialized
|
||||
(smallstep/certificates#1223).
|
||||
|
||||
### Changed
|
||||
|
||||
- Ignore SSH principals validation when using an OIDC provisioner. The
|
||||
provisioner will ignore the principals passed and set the defaults or the ones
|
||||
including using WebHooks or templates (smallstep/certificates#1206).
|
||||
|
||||
## [v0.23.0] - 2022-11-11
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for ACME device-attest-01 challenge on iOS, iPadOS, tvOS and
|
||||
YubiKey.
|
||||
- Ability to disable ACME challenges and attestation formats.
|
||||
- Added flags to change ACME challenge ports for testing purposes.
|
||||
- Added name constraints evaluation and enforcement when issuing or renewing
|
||||
X.509 certificates.
|
||||
- Added provisioner webhooks for augmenting template data and authorizing
|
||||
certificate requests before signing.
|
||||
- Added automatic migration of provisioners when enabling remote management.
|
||||
- Added experimental support for CRLs.
|
||||
- Add certificate renewal support on RA mode. The `step ca renew` command must
|
||||
use the flag `--mtls=false` to use the token renewal flow.
|
||||
- Added support for initializing remote management using `step ca init`.
|
||||
- Added support for renewing X.509 certificates on RAs.
|
||||
- Added support for using SCEP with keys in a KMS.
|
||||
- Added client support to set the dialer's local address with the environment variable
|
||||
`STEP_CLIENT_ADDR`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove the email requirement for issuing SSH certificates with an OIDC
|
||||
provisioner.
|
||||
- Root files can contain more than one certificate.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed MySQL DSN parsing issues with an upgrade to
|
||||
[smallstep/nosql@v0.5.0](https://github.com/smallstep/nosql/releases/tag/v0.5.0).
|
||||
- Fixed renewal of certificates with missing subject attributes.
|
||||
- Fixed ACME support with [ejabberd](https://github.com/processone/ejabberd).
|
||||
|
||||
### Deprecated
|
||||
|
||||
- The CLIs `step-awskms-init`, `step-cloudkms-init`, `step-pkcs11-init`,
|
||||
`step-yubikey-init` are deprecated. Now you can use
|
||||
[`step-kms-plugin`](https://github.com/smallstep/step-kms-plugin) in
|
||||
combination with `step certificates create` to initialize your PKI.
|
||||
|
||||
## [0.22.1] - 2022-08-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed signature algorithm on EC (root) + RSA (intermediate) PKIs.
|
||||
|
||||
## [0.22.0] - 2022-08-26
|
||||
|
||||
### Added
|
||||
|
||||
- Added automatic configuration of Linked RAs.
|
||||
- Send provisioner configuration on Linked RAs.
|
||||
|
||||
### Changed
|
||||
|
||||
- Certificates signed by an issuer using an RSA key will be signed using the
|
||||
same algorithm used to sign the issuer certificate. The signature will no
|
||||
longer default to PKCS #1. For example, if the issuer certificate was signed
|
||||
|
@ -39,20 +206,28 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||
- Sanitize TLS options.
|
||||
|
||||
## [0.20.0] - 2022-05-26
|
||||
|
||||
### Added
|
||||
|
||||
- Added Kubernetes auth method for Vault RAs.
|
||||
- Added support for reporting provisioners to linkedca.
|
||||
- Added support for certificate policies on authority level.
|
||||
- Added a Dockerfile with a step-ca build with HSM support.
|
||||
- A few new WithXX methods for instantiating authorities
|
||||
|
||||
### Changed
|
||||
|
||||
- Context usage in HTTP APIs.
|
||||
- Changed authentication for Vault RAs.
|
||||
- Error message returned to client when authenticating with expired certificate.
|
||||
- Strip padding from ACME CSRs.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- HTTP API handler types.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed SSH revocation.
|
||||
- CA client dial context for js/wasm target.
|
||||
- Incomplete `extraNames` support in templates.
|
||||
|
@ -60,7 +235,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||
- Large SCEP request handling.
|
||||
|
||||
## [0.19.0] - 2022-04-19
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for certificate renewals after expiry using the claim `allowRenewalAfterExpiry`.
|
||||
- Added support for `extraNames` in X.509 templates.
|
||||
- Added `armv5` builds.
|
||||
|
@ -69,110 +246,162 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||
- Added a new `/roots.pem` endpoint to download the CA roots in PEM format.
|
||||
- Added support for Azure `Managed Identity` tokens.
|
||||
- Added support for automatic configuration of linked RAs.
|
||||
- Added support for the `--context` flag. It's now possible to start the
|
||||
- Added support for the `--context` flag. It's now possible to start the
|
||||
CA with `step-ca --context=abc` to use the configuration from context `abc`.
|
||||
When a context has been configured and no configuration file is provided
|
||||
on startup, the configuration for the current context is used.
|
||||
- Added startup info logging and option to skip it (`--quiet`).
|
||||
- Added support for renaming the CA (Common Name).
|
||||
|
||||
### Changed
|
||||
|
||||
- Made SCEP CA URL paths dynamic.
|
||||
- Support two latest versions of Go (1.17, 1.18).
|
||||
- Upgrade go.step.sm/crypto to v0.16.1.
|
||||
- Upgrade go.step.sm/linkedca to v0.15.0.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Go 1.16 support.
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed admin credentials on RAs.
|
||||
- Fixed ACME HTTP-01 challenges for IPv6 identifiers.
|
||||
- Various improvements under the hood.
|
||||
|
||||
### Security
|
||||
|
||||
## [0.18.2] - 2022-03-01
|
||||
|
||||
### Added
|
||||
|
||||
- Added `subscriptionIDs` and `objectIDs` filters to the Azure provisioner.
|
||||
- [NoSQL](https://github.com/smallstep/nosql/pull/21) package allows filtering
|
||||
out database drivers using Go tags. For example, using the Go flag
|
||||
`--tags=nobadger,nobbolt,nomysql` will only compile `step-ca` with the pgx
|
||||
driver for PostgreSQL.
|
||||
|
||||
### Changed
|
||||
|
||||
- IPv6 addresses are normalized as IP addresses instead of hostnames.
|
||||
- More descriptive JWK decryption error message.
|
||||
- Make the X5C leaf certificate available to the templates using `{{ .AuthorizationCrt }}`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- During provisioner add - validate provisioner configuration before storing to DB.
|
||||
|
||||
## [0.18.1] - 2022-02-03
|
||||
|
||||
### Added
|
||||
|
||||
- Support for ACME revocation.
|
||||
- Replace hash function with an RSA SSH CA to "rsa-sha2-256".
|
||||
- Support Nebula provisioners.
|
||||
- Example Ansible configurations.
|
||||
- Support PKCS#11 as a decrypter, as used by SCEP.
|
||||
|
||||
### Changed
|
||||
|
||||
- Automatically create database directory on `step ca init`.
|
||||
- Slightly improve errors reported when a template has invalid content.
|
||||
- Error reporting in logs and to clients.
|
||||
|
||||
### Fixed
|
||||
|
||||
- SCEP renewal using HTTPS on macOS.
|
||||
|
||||
## [0.18.0] - 2021-11-17
|
||||
|
||||
### Added
|
||||
|
||||
- Support for multiple certificate authority contexts.
|
||||
- Support for generating extractable keys and certificates on a pkcs#11 module.
|
||||
|
||||
### Changed
|
||||
|
||||
- Support two latest versions of Go (1.16, 1.17)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- go 1.15 support
|
||||
|
||||
## [0.17.6] - 2021-10-20
|
||||
|
||||
### Notes
|
||||
|
||||
- 0.17.5 failed in CI/CD
|
||||
|
||||
## [0.17.5] - 2021-10-20
|
||||
|
||||
### Added
|
||||
|
||||
- Support for Azure Key Vault as a KMS.
|
||||
- Adapt `pki` package to support key managers.
|
||||
- gocritic linter
|
||||
|
||||
### Fixed
|
||||
|
||||
- gocritic warnings
|
||||
|
||||
## [0.17.4] - 2021-09-28
|
||||
|
||||
### Fixed
|
||||
|
||||
- Support host-only or user-only SSH CA.
|
||||
|
||||
## [0.17.3] - 2021-09-24
|
||||
|
||||
### Added
|
||||
|
||||
- go 1.17 to github action test matrix
|
||||
- Support for CloudKMS RSA-PSS signers without using templates.
|
||||
- Add flags to support individual passwords for the intermediate and SSH keys.
|
||||
- Global support for group admins in the OIDC provisioner.
|
||||
|
||||
### Changed
|
||||
|
||||
- Using go 1.17 for binaries
|
||||
|
||||
### Fixed
|
||||
|
||||
- Upgrade go-jose.v2 to fix a bug in the JWK fingerprint of Ed25519 keys.
|
||||
|
||||
### Security
|
||||
|
||||
- Use cosign to sign and upload signatures for multi-arch Docker container.
|
||||
- Add debian checksum
|
||||
|
||||
## [0.17.2] - 2021-08-30
|
||||
|
||||
### Added
|
||||
|
||||
- Additional way to distinguish Azure IID and Azure OIDC tokens.
|
||||
|
||||
### Security
|
||||
|
||||
- Sign over all goreleaser github artifacts using cosign
|
||||
|
||||
## [0.17.1] - 2021-08-26
|
||||
|
||||
## [0.17.0] - 2021-08-25
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for Linked CAs using protocol buffers and gRPC
|
||||
- `step-ca init` adds support for
|
||||
- configuring a StepCAS RA
|
||||
- configuring a Linked CA
|
||||
- congifuring a `step-ca` using Helm
|
||||
|
||||
### Changed
|
||||
|
||||
- Update badger driver to use v2 by default
|
||||
- Update TLS cipher suites to include 1.3
|
||||
|
||||
### Security
|
||||
|
||||
- Fix key version when SHA512WithRSA is used. There was a typo creating RSA keys with SHA256 digests instead of SHA512.
|
||||
|
|
|
@ -74,7 +74,7 @@ sudo yum install pcsc-lite-devel
|
|||
To build `step-ca`, clone this repository and run the following:
|
||||
|
||||
```shell
|
||||
make bootstrap && make build GOFLAGS=""
|
||||
make bootstrap && make build GO_ENVS="CGO_ENABLED=1"
|
||||
```
|
||||
|
||||
When the build is complete, you will find binaries in `bin/`.
|
150
Makefile
150
Makefile
|
@ -1,21 +1,11 @@
|
|||
PKG?=github.com/smallstep/certificates/cmd/step-ca
|
||||
BINNAME?=step-ca
|
||||
CLOUDKMS_BINNAME?=step-cloudkms-init
|
||||
CLOUDKMS_PKG?=github.com/smallstep/certificates/cmd/step-cloudkms-init
|
||||
AWSKMS_BINNAME?=step-awskms-init
|
||||
AWSKMS_PKG?=github.com/smallstep/certificates/cmd/step-awskms-init
|
||||
YUBIKEY_BINNAME?=step-yubikey-init
|
||||
YUBIKEY_PKG?=github.com/smallstep/certificates/cmd/step-yubikey-init
|
||||
PKCS11_BINNAME?=step-pkcs11-init
|
||||
PKCS11_PKG?=github.com/smallstep/certificates/cmd/step-pkcs11-init
|
||||
|
||||
# Set V to 1 for verbose output from the Makefile
|
||||
Q=$(if $V,,@)
|
||||
PREFIX?=
|
||||
SRC=$(shell find . -type f -name '*.go' -not -path "./vendor/*")
|
||||
GOOS_OVERRIDE ?=
|
||||
OUTPUT_ROOT=output/
|
||||
RELEASE=./.releases
|
||||
|
||||
all: lint test build
|
||||
|
||||
|
@ -28,8 +18,11 @@ ci: testcgo build
|
|||
#########################################
|
||||
|
||||
bootstra%:
|
||||
# Using a released version of golangci-lint to take into account custom replacements in their go.mod
|
||||
$Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.42.0
|
||||
$Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin latest
|
||||
$Q go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
$Q go install gotest.tools/gotestsum@latest
|
||||
$Q go install github.com/goreleaser/goreleaser@latest
|
||||
$Q go install github.com/sigstore/cosign/v2/cmd/cosign@latest
|
||||
|
||||
.PHONY: bootstra%
|
||||
|
||||
|
@ -37,17 +30,8 @@ bootstra%:
|
|||
# Determine the type of `push` and `version`
|
||||
#################################################
|
||||
|
||||
# If TRAVIS_TAG is set then we know this ref has been tagged.
|
||||
ifdef TRAVIS_TAG
|
||||
VERSION ?= $(TRAVIS_TAG)
|
||||
NOT_RC := $(shell echo $(VERSION) | grep -v -e -rc)
|
||||
ifeq ($(NOT_RC),)
|
||||
PUSHTYPE := release-candidate
|
||||
else
|
||||
PUSHTYPE := release
|
||||
endif
|
||||
# GITHUB Actions
|
||||
else ifdef GITHUB_REF
|
||||
ifdef GITHUB_REF
|
||||
VERSION ?= $(shell echo $(GITHUB_REF) | sed 's/^refs\/tags\///')
|
||||
NOT_RC := $(shell echo $(VERSION) | grep -v -e -rc)
|
||||
ifeq ($(NOT_RC),)
|
||||
|
@ -60,59 +44,50 @@ VERSION ?= $(shell [ -d .git ] && git describe --tags --always --dirty="-dev")
|
|||
# If we are not in an active git dir then try reading the version from .VERSION.
|
||||
# .VERSION contains a slug populated by `git archive`.
|
||||
VERSION := $(or $(VERSION),$(shell ./.version.sh .VERSION))
|
||||
ifeq ($(TRAVIS_BRANCH),master)
|
||||
PUSHTYPE := master
|
||||
else
|
||||
PUSHTYPE := branch
|
||||
endif
|
||||
endif
|
||||
|
||||
VERSION := $(shell echo $(VERSION) | sed 's/^v//')
|
||||
DEB_VERSION := $(shell echo $(VERSION) | sed 's/-/./g')
|
||||
|
||||
ifdef V
|
||||
$(info TRAVIS_TAG is $(TRAVIS_TAG))
|
||||
$(info GITHUB_REF is $(GITHUB_REF))
|
||||
$(info VERSION is $(VERSION))
|
||||
$(info DEB_VERSION is $(DEB_VERSION))
|
||||
$(info PUSHTYPE is $(PUSHTYPE))
|
||||
endif
|
||||
|
||||
include make/docker.mk
|
||||
|
||||
#########################################
|
||||
# Build
|
||||
#########################################
|
||||
|
||||
DATE := $(shell date -u '+%Y-%m-%d %H:%M UTC')
|
||||
LDFLAGS := -ldflags='-w -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'
|
||||
GOFLAGS := CGO_ENABLED=0
|
||||
|
||||
# Always explicitly enable or disable cgo,
|
||||
# so that go doesn't silently fall back on
|
||||
# non-cgo when gcc is not found.
|
||||
ifeq (,$(findstring CGO_ENABLED,$(GO_ENVS)))
|
||||
ifneq ($(origin GOFLAGS),undefined)
|
||||
# This section is for backward compatibility with
|
||||
#
|
||||
# $ make build GOFLAGS=""
|
||||
#
|
||||
# which is how we recommended building step-ca with cgo support
|
||||
# until June 2023.
|
||||
GO_ENVS := $(GO_ENVS) CGO_ENABLED=1
|
||||
else
|
||||
GO_ENVS := $(GO_ENVS) CGO_ENABLED=0
|
||||
endif
|
||||
endif
|
||||
|
||||
download:
|
||||
$Q go mod download
|
||||
|
||||
build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(PREFIX)bin/$(AWSKMS_BINNAME) $(PREFIX)bin/$(YUBIKEY_BINNAME) $(PREFIX)bin/$(PKCS11_BINNAME)
|
||||
build: $(PREFIX)bin/$(BINNAME)
|
||||
@echo "Build Complete!"
|
||||
|
||||
$(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go)
|
||||
$Q mkdir -p $(@D)
|
||||
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG)
|
||||
|
||||
$(PREFIX)bin/$(CLOUDKMS_BINNAME): download $(call rwildcard,*.go)
|
||||
$Q mkdir -p $(@D)
|
||||
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(LDFLAGS) $(CLOUDKMS_PKG)
|
||||
|
||||
$(PREFIX)bin/$(AWSKMS_BINNAME): download $(call rwildcard,*.go)
|
||||
$Q mkdir -p $(@D)
|
||||
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(AWSKMS_BINNAME) $(LDFLAGS) $(AWSKMS_PKG)
|
||||
|
||||
$(PREFIX)bin/$(YUBIKEY_BINNAME): download $(call rwildcard,*.go)
|
||||
$Q mkdir -p $(@D)
|
||||
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(YUBIKEY_BINNAME) $(LDFLAGS) $(YUBIKEY_PKG)
|
||||
|
||||
$(PREFIX)bin/$(PKCS11_BINNAME): download $(call rwildcard,*.go)
|
||||
$Q mkdir -p $(@D)
|
||||
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(PKCS11_BINNAME) $(LDFLAGS) $(PKCS11_PKG)
|
||||
$Q $(GOOS_OVERRIDE) GOFLAGS="$(GOFLAGS)" $(GO_ENVS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG)
|
||||
|
||||
# Target to force a build of step-ca without running tests
|
||||
simple: build
|
||||
|
@ -131,18 +106,26 @@ generate:
|
|||
#########################################
|
||||
# Test
|
||||
#########################################
|
||||
test:
|
||||
$Q $(GOFLAGS) go test -short -coverprofile=coverage.out ./...
|
||||
test: testdefault testtpmsimulator combinecoverage
|
||||
|
||||
testdefault:
|
||||
$Q $(GO_ENVS) gotestsum -- -coverprofile=defaultcoverage.out -short -covermode=atomic ./...
|
||||
|
||||
testtpmsimulator:
|
||||
$Q CGO_ENABLED=1 gotestsum -- -coverprofile=tpmsimulatorcoverage.out -short -covermode=atomic -tags tpmsimulator ./acme
|
||||
|
||||
testcgo:
|
||||
$Q go test -short -coverprofile=coverage.out ./...
|
||||
$Q gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./...
|
||||
|
||||
.PHONY: test testcgo
|
||||
combinecoverage:
|
||||
cat defaultcoverage.out tpmsimulatorcoverage.out > coverage.out
|
||||
|
||||
.PHONY: test testdefault testtpmsimulator testcgo combinecoverage
|
||||
|
||||
integrate: integration
|
||||
|
||||
integration: bin/$(BINNAME)
|
||||
$Q $(GOFLAGS) go test -tags=integration ./integration/...
|
||||
$Q $(GO_ENVS) gotestsum -- -tags=integration ./integration/...
|
||||
|
||||
.PHONY: integrate integration
|
||||
|
||||
|
@ -151,15 +134,14 @@ integration: bin/$(BINNAME)
|
|||
#########################################
|
||||
|
||||
fmt:
|
||||
$Q gofmt -l -s -w $(SRC)
|
||||
$Q goimports -l -w $(SRC)
|
||||
|
||||
lint: SHELL:=/bin/bash
|
||||
lint:
|
||||
$Q golangci-lint run --timeout=30m
|
||||
$Q LOG_LEVEL=error golangci-lint run --config <(curl -s https://raw.githubusercontent.com/smallstep/workflows/master/.golangci.yml) --timeout=30m
|
||||
$Q govulncheck ./...
|
||||
|
||||
lintcgo:
|
||||
$Q LOG_LEVEL=error golangci-lint run --timeout=30m
|
||||
|
||||
.PHONY: fmt lint lintcgo
|
||||
.PHONY: fmt lint
|
||||
|
||||
#########################################
|
||||
# Install
|
||||
|
@ -167,15 +149,11 @@ lintcgo:
|
|||
|
||||
INSTALL_PREFIX?=/usr/
|
||||
|
||||
install: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(PREFIX)bin/$(AWSKMS_BINNAME)
|
||||
install: $(PREFIX)bin/$(BINNAME)
|
||||
$Q install -D $(PREFIX)bin/$(BINNAME) $(DESTDIR)$(INSTALL_PREFIX)bin/$(BINNAME)
|
||||
$Q install -D $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(DESTDIR)$(INSTALL_PREFIX)bin/$(CLOUDKMS_BINNAME)
|
||||
$Q install -D $(PREFIX)bin/$(AWSKMS_BINNAME) $(DESTDIR)$(INSTALL_PREFIX)bin/$(AWSKMS_BINNAME)
|
||||
|
||||
uninstall:
|
||||
$Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BINNAME)
|
||||
$Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(CLOUDKMS_BINNAME)
|
||||
$Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(AWSKMS_BINNAME)
|
||||
|
||||
.PHONY: install uninstall
|
||||
|
||||
|
@ -187,18 +165,6 @@ clean:
|
|||
ifneq ($(BINNAME),"")
|
||||
$Q rm -f bin/$(BINNAME)
|
||||
endif
|
||||
ifneq ($(CLOUDKMS_BINNAME),"")
|
||||
$Q rm -f bin/$(CLOUDKMS_BINNAME)
|
||||
endif
|
||||
ifneq ($(AWSKMS_BINNAME),"")
|
||||
$Q rm -f bin/$(AWSKMS_BINNAME)
|
||||
endif
|
||||
ifneq ($(YUBIKEY_BINNAME),"")
|
||||
$Q rm -f bin/$(YUBIKEY_BINNAME)
|
||||
endif
|
||||
ifneq ($(PKCS11_BINNAME),"")
|
||||
$Q rm -f bin/$(PKCS11_BINNAME)
|
||||
endif
|
||||
|
||||
.PHONY: clean
|
||||
|
||||
|
@ -211,31 +177,3 @@ run:
|
|||
|
||||
.PHONY: run
|
||||
|
||||
#########################################
|
||||
# Debian
|
||||
#########################################
|
||||
|
||||
changelog:
|
||||
$Q echo "step-ca ($(DEB_VERSION)) unstable; urgency=medium" > debian/changelog
|
||||
$Q echo >> debian/changelog
|
||||
$Q echo " * See https://github.com/smallstep/certificates/releases" >> debian/changelog
|
||||
$Q echo >> debian/changelog
|
||||
$Q echo " -- Smallstep Labs, Inc. <techadmin@smallstep.com> $(shell date -uR)" >> debian/changelog
|
||||
|
||||
debian: changelog
|
||||
$Q mkdir -p $(RELEASE); \
|
||||
OUTPUT=../step-ca*.deb; \
|
||||
rm $$OUTPUT; \
|
||||
dpkg-buildpackage -b -rfakeroot -us -uc && cp $$OUTPUT $(RELEASE)/
|
||||
|
||||
distclean: clean
|
||||
|
||||
.PHONY: changelog debian distclean
|
||||
|
||||
#################################################
|
||||
# Targets for creating step artifacts
|
||||
#################################################
|
||||
|
||||
docker-artifacts: docker-$(PUSHTYPE)
|
||||
|
||||
.PHONY: docker-artifacts
|
||||
|
|
14
README.md
14
README.md
|
@ -119,18 +119,12 @@ See our installation docs [here](https://smallstep.com/docs/step-ca/installation
|
|||
|
||||
## Documentation
|
||||
|
||||
Documentation can be found in a handful of different places:
|
||||
|
||||
1. On the web at https://smallstep.com/docs/step-ca.
|
||||
|
||||
2. On the command line with `step help ca xxx` where `xxx` is the subcommand
|
||||
you are interested in. Ex: `step help ca provisioner list`.
|
||||
|
||||
3. In your browser, by running `step help --http=:8080 ca` from the command line
|
||||
* [Official documentation](https://smallstep.com/docs/step-ca) is on smallstep.com
|
||||
* The `step` command reference is available via `step help`,
|
||||
[on smallstep.com](https://smallstep.com/docs/step-cli/reference/),
|
||||
or by running `step help --http=:8080` from the command line
|
||||
and visiting http://localhost:8080.
|
||||
|
||||
4. The [docs](./docs/README.md) folder is being deprecated, but it still has some documentation and tutorials.
|
||||
|
||||
## Feedback?
|
||||
|
||||
* Tell us what you like and don't like about managing your PKI - we're eager to help solve problems in this space.
|
||||
|
|
|
@ -20,6 +20,16 @@ type Account struct {
|
|||
Status Status `json:"status"`
|
||||
OrdersURL string `json:"orders"`
|
||||
ExternalAccountBinding interface{} `json:"externalAccountBinding,omitempty"`
|
||||
LocationPrefix string `json:"-"`
|
||||
ProvisionerName string `json:"-"`
|
||||
}
|
||||
|
||||
// GetLocation returns the URL location of the given account.
|
||||
func (a *Account) GetLocation() string {
|
||||
if a.LocationPrefix == "" {
|
||||
return ""
|
||||
}
|
||||
return a.LocationPrefix + a.ID
|
||||
}
|
||||
|
||||
// ToLog enables response logging.
|
||||
|
@ -33,7 +43,7 @@ func (a *Account) ToLog() (interface{}, error) {
|
|||
|
||||
// IsValid returns true if the Account is valid.
|
||||
func (a *Account) IsValid() bool {
|
||||
return Status(a.Status) == StatusValid
|
||||
return a.Status == StatusValid
|
||||
}
|
||||
|
||||
// KeyToID converts a JWK to a thumbprint.
|
||||
|
@ -72,6 +82,7 @@ func (p *Policy) GetAllowedNameOptions() *policy.X509NameOptions {
|
|||
IPRanges: p.X509.Allowed.IPRanges,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Policy) GetDeniedNameOptions() *policy.X509NameOptions {
|
||||
if p == nil {
|
||||
return nil
|
||||
|
|
|
@ -46,14 +46,14 @@ func TestKeyToID(t *testing.T) {
|
|||
tc := run(t)
|
||||
if id, err := KeyToID(tc.jwk); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
@ -66,6 +66,23 @@ func TestKeyToID(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAccount_GetLocation(t *testing.T) {
|
||||
locationPrefix := "https://test.ca.smallstep.com/acme/foo/account/"
|
||||
type test struct {
|
||||
acc *Account
|
||||
exp string
|
||||
}
|
||||
tests := map[string]test{
|
||||
"empty": {acc: &Account{LocationPrefix: ""}, exp: ""},
|
||||
"not-empty": {acc: &Account{ID: "bar", LocationPrefix: locationPrefix}, exp: locationPrefix + "bar"},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equals(t, tc.acc.GetLocation(), tc.exp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccount_IsValid(t *testing.T) {
|
||||
type test struct {
|
||||
acc *Account
|
||||
|
@ -131,12 +148,12 @@ func TestExternalAccountKey_BindTo(t *testing.T) {
|
|||
}
|
||||
if wantErr {
|
||||
assert.NotNil(t, err)
|
||||
assert.Type(t, &Error{}, err)
|
||||
ae, _ := err.(*Error)
|
||||
assert.Equals(t, ae.Type, tt.err.Type)
|
||||
assert.Equals(t, ae.Detail, tt.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tt.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tt.err.Subproblems)
|
||||
var ae *Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.Equals(t, ae.Type, tt.err.Type)
|
||||
assert.Equals(t, ae.Detail, tt.err.Detail)
|
||||
assert.Equals(t, ae.Subproblems, tt.err.Subproblems)
|
||||
}
|
||||
} else {
|
||||
assert.Equals(t, eak.AccountID, acct.ID)
|
||||
assert.Equals(t, eak.HmacKey, []byte{})
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
@ -66,6 +68,12 @@ func (u *UpdateAccountRequest) Validate() error {
|
|||
}
|
||||
}
|
||||
|
||||
// getAccountLocationPath returns the current account URL location.
|
||||
// Returned location will be of the form: https://<ca-url>/acme/<provisioner>/account/<accID>
|
||||
func getAccountLocationPath(ctx context.Context, linker acme.Linker, accID string) string {
|
||||
return linker.GetLink(ctx, acme.AccountLinkType, accID)
|
||||
}
|
||||
|
||||
// NewAccount is the handler resource for creating new ACME accounts.
|
||||
func NewAccount(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
@ -97,8 +105,8 @@ func NewAccount(w http.ResponseWriter, r *http.Request) {
|
|||
httpStatus := http.StatusCreated
|
||||
acc, err := accountFromContext(ctx)
|
||||
if err != nil {
|
||||
acmeErr, ok := err.(*acme.Error)
|
||||
if !ok || acmeErr.Status != http.StatusBadRequest {
|
||||
var acmeErr *acme.Error
|
||||
if !errors.As(err, &acmeErr) || acmeErr.Status != http.StatusBadRequest {
|
||||
// Something went wrong ...
|
||||
render.Error(w, err)
|
||||
return
|
||||
|
@ -124,9 +132,11 @@ func NewAccount(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
acc = &acme.Account{
|
||||
Key: jwk,
|
||||
Contact: nar.Contact,
|
||||
Status: acme.StatusValid,
|
||||
Key: jwk,
|
||||
Contact: nar.Contact,
|
||||
Status: acme.StatusValid,
|
||||
LocationPrefix: getAccountLocationPath(ctx, linker, ""),
|
||||
ProvisionerName: prov.GetName(),
|
||||
}
|
||||
if err := db.CreateAccount(ctx, acc); err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error creating account"))
|
||||
|
@ -151,7 +161,7 @@ func NewAccount(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
linker.LinkAccount(ctx, acc)
|
||||
|
||||
w.Header().Set("Location", linker.GetLink(r.Context(), acme.AccountLinkType, acc.ID))
|
||||
w.Header().Set("Location", getAccountLocationPath(ctx, linker, acc.ID))
|
||||
render.JSONStatus(w, acc, httpStatus)
|
||||
}
|
||||
|
||||
|
|
|
@ -34,31 +34,24 @@ var (
|
|||
|
||||
type fakeProvisioner struct{}
|
||||
|
||||
func (*fakeProvisioner) AuthorizeOrderIdentifier(ctx context.Context, identifier provisioner.ACMEIdentifier) error {
|
||||
func (*fakeProvisioner) AuthorizeOrderIdentifier(context.Context, provisioner.ACMEIdentifier) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*fakeProvisioner) AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error) {
|
||||
func (*fakeProvisioner) AuthorizeSign(context.Context, string) ([]provisioner.SignOption, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (*fakeProvisioner) IsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool {
|
||||
func (*fakeProvisioner) IsChallengeEnabled(context.Context, provisioner.ACMEChallenge) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (*fakeProvisioner) IsAttestationFormatEnabled(ctx context.Context, format provisioner.ACMEAttestationFormat) bool {
|
||||
func (*fakeProvisioner) IsAttestationFormatEnabled(context.Context, provisioner.ACMEAttestationFormat) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (*fakeProvisioner) GetAttestationRoots() (*x509.CertPool, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (*fakeProvisioner) AuthorizeRevoke(ctx context.Context, token string) error { return nil }
|
||||
func (*fakeProvisioner) GetID() string { return "" }
|
||||
func (*fakeProvisioner) GetName() string { return "" }
|
||||
func (*fakeProvisioner) DefaultTLSCertDuration() time.Duration { return 0 }
|
||||
func (*fakeProvisioner) GetOptions() *provisioner.Options { return nil }
|
||||
func (*fakeProvisioner) GetAttestationRoots() (*x509.CertPool, bool) { return nil, false }
|
||||
func (*fakeProvisioner) AuthorizeRevoke(context.Context, string) error { return nil }
|
||||
func (*fakeProvisioner) GetID() string { return "" }
|
||||
func (*fakeProvisioner) GetName() string { return "" }
|
||||
func (*fakeProvisioner) DefaultTLSCertDuration() time.Duration { return 0 }
|
||||
func (*fakeProvisioner) GetOptions() *provisioner.Options { return nil }
|
||||
|
||||
func newProv() acme.Provisioner {
|
||||
// Initialize provisioners
|
||||
|
@ -197,11 +190,12 @@ func TestNewAccountRequest_Validate(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.nar.Validate(); err != nil {
|
||||
if assert.NotNil(t, err) {
|
||||
ae, ok := err.(*acme.Error)
|
||||
assert.True(t, ok)
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
var ae *acme.Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
|
@ -268,11 +262,12 @@ func TestUpdateAccountRequest_Validate(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.uar.Validate(); err != nil {
|
||||
if assert.NotNil(t, err) {
|
||||
ae, ok := err.(*acme.Error)
|
||||
assert.True(t, ok)
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
var ae *acme.Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
|
@ -367,7 +362,7 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) {
|
|||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil)
|
||||
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil, "")
|
||||
req := httptest.NewRequest("GET", u, nil)
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
@ -386,7 +381,6 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -807,7 +801,7 @@ func TestHandler_NewAccount(t *testing.T) {
|
|||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil)
|
||||
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil, "")
|
||||
req := httptest.NewRequest("GET", "/foo/bar", nil)
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
@ -826,7 +820,6 @@ func TestHandler_NewAccount(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -1011,7 +1004,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) {
|
|||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil)
|
||||
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil, "")
|
||||
req := httptest.NewRequest("GET", "/foo/bar", nil)
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
@ -1030,7 +1023,6 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
|
||||
|
@ -24,6 +25,7 @@ func validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest)
|
|||
}
|
||||
|
||||
if !acmeProv.RequireEAB {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -51,7 +53,8 @@ func validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest)
|
|||
db := acme.MustDatabaseFromContext(ctx)
|
||||
externalAccountKey, err := db.GetExternalAccountKey(ctx, acmeProv.ID, keyID)
|
||||
if err != nil {
|
||||
if _, ok := err.(*acme.Error); ok {
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key")
|
||||
}
|
||||
return nil, acme.WrapErrorISE(err, "error retrieving external account key")
|
||||
|
|
|
@ -860,13 +860,14 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
|||
if wantErr {
|
||||
assert.NotNil(t, err)
|
||||
assert.Type(t, &acme.Error{}, err)
|
||||
ae, _ := err.(*acme.Error)
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Status, tc.err.Status)
|
||||
assert.HasPrefix(t, ae.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
var ae *acme.Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Status, tc.err.Status)
|
||||
assert.HasPrefix(t, ae.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
}
|
||||
} else {
|
||||
if got == nil {
|
||||
assert.Nil(t, tc.eak)
|
||||
|
@ -1143,7 +1144,6 @@ func Test_validateEABJWS(t *testing.T) {
|
|||
assert.Equals(t, tc.err.Status, err.Status)
|
||||
assert.HasPrefix(t, err.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, tc.err.Detail, err.Detail)
|
||||
assert.Equals(t, tc.err.Identifier, err.Identifier)
|
||||
assert.Equals(t, tc.err.Subproblems, err.Subproblems)
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
|
|
|
@ -95,7 +95,7 @@ func (h *handler) Route(r api.Router) {
|
|||
if ca, ok := h.opts.CA.(*authority.Authority); ok && ca != nil {
|
||||
ctx = authority.NewContext(ctx, ca)
|
||||
}
|
||||
ctx = acme.NewContext(ctx, h.opts.DB, client, linker, h.opts.PrerequisitesChecker)
|
||||
ctx = acme.NewContext(ctx, h.opts.DB, client, linker, h.opts.PrerequisitesChecker, "")
|
||||
next(w, r.WithContext(ctx))
|
||||
}
|
||||
})
|
||||
|
@ -205,7 +205,7 @@ type Directory struct {
|
|||
NewOrder string `json:"newOrder"`
|
||||
RevokeCert string `json:"revokeCert"`
|
||||
KeyChange string `json:"keyChange"`
|
||||
Meta Meta `json:"meta"`
|
||||
Meta *Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// ToLog enables response logging for the Directory type.
|
||||
|
@ -228,21 +228,52 @@ func GetDirectory(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
render.JSON(w, &Directory{
|
||||
NewNonce: linker.GetLink(ctx, acme.NewNonceLinkType),
|
||||
NewAccount: linker.GetLink(ctx, acme.NewAccountLinkType),
|
||||
NewOrder: linker.GetLink(ctx, acme.NewOrderLinkType),
|
||||
RevokeCert: linker.GetLink(ctx, acme.RevokeCertLinkType),
|
||||
KeyChange: linker.GetLink(ctx, acme.KeyChangeLinkType),
|
||||
Meta: Meta{
|
||||
ExternalAccountRequired: acmeProv.RequireEAB,
|
||||
},
|
||||
Meta: createMetaObject(acmeProv),
|
||||
})
|
||||
}
|
||||
|
||||
// createMetaObject creates a Meta object if the ACME provisioner
|
||||
// has one or more properties that are written in the ACME directory output.
|
||||
// It returns nil if none of the properties are set.
|
||||
func createMetaObject(p *provisioner.ACME) *Meta {
|
||||
if shouldAddMetaObject(p) {
|
||||
return &Meta{
|
||||
TermsOfService: p.TermsOfService,
|
||||
Website: p.Website,
|
||||
CaaIdentities: p.CaaIdentities,
|
||||
ExternalAccountRequired: p.RequireEAB,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldAddMetaObject returns whether or not the ACME provisioner
|
||||
// has properties configured that must be added to the ACME directory object.
|
||||
func shouldAddMetaObject(p *provisioner.ACME) bool {
|
||||
switch {
|
||||
case p.TermsOfService != "":
|
||||
return true
|
||||
case p.Website != "":
|
||||
return true
|
||||
case len(p.CaaIdentities) > 0:
|
||||
return true
|
||||
case p.RequireEAB:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// NotImplemented returns a 501 and is generally a placeholder for functionality which
|
||||
// MAY be added at some point in the future but is not in any way a guarantee of such.
|
||||
func NotImplemented(w http.ResponseWriter, r *http.Request) {
|
||||
func NotImplemented(w http.ResponseWriter, _ *http.Request) {
|
||||
render.Error(w, acme.NewError(acme.ErrorNotImplementedType, "this API is not implemented"))
|
||||
}
|
||||
|
||||
|
@ -363,6 +394,6 @@ func GetCertificate(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
api.LogCertificate(w, cert.Leaf)
|
||||
w.Header().Set("Content-Type", "application/pem-certificate-chain; charset=utf-8")
|
||||
w.Header().Set("Content-Type", "application/pem-certificate-chain")
|
||||
w.Write(certBytes)
|
||||
}
|
||||
|
|
|
@ -18,10 +18,13 @@ import (
|
|||
"github.com/go-chi/chi"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
)
|
||||
|
||||
type mockClient struct {
|
||||
|
@ -129,7 +132,35 @@ func TestHandler_GetDirectory(t *testing.T) {
|
|||
NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName),
|
||||
RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName),
|
||||
KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName),
|
||||
Meta: Meta{
|
||||
Meta: &Meta{
|
||||
ExternalAccountRequired: true,
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
dir: expDir,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
"ok/full-meta": func(t *testing.T) test {
|
||||
prov := newACMEProv(t)
|
||||
prov.TermsOfService = "https://terms.ca.local/"
|
||||
prov.Website = "https://ca.local/"
|
||||
prov.CaaIdentities = []string{"ca.local"}
|
||||
prov.RequireEAB = true
|
||||
provName := url.PathEscape(prov.GetName())
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
expDir := Directory{
|
||||
NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName),
|
||||
NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName),
|
||||
NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName),
|
||||
RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName),
|
||||
KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName),
|
||||
Meta: &Meta{
|
||||
TermsOfService: "https://terms.ca.local/",
|
||||
Website: "https://ca.local/",
|
||||
CaaIdentities: []string{"ca.local"},
|
||||
ExternalAccountRequired: true,
|
||||
},
|
||||
}
|
||||
|
@ -162,7 +193,6 @@ func TestHandler_GetDirectory(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -316,7 +346,7 @@ func TestHandler_GetAuthorization(t *testing.T) {
|
|||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil)
|
||||
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil, "")
|
||||
req := httptest.NewRequest("GET", "/foo/bar", nil)
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
@ -335,7 +365,6 @@ func TestHandler_GetAuthorization(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -478,12 +507,11 @@ func TestHandler_GetCertificate(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.HasPrefix(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
assert.Equals(t, bytes.TrimSpace(body), bytes.TrimSpace(certBytes))
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/pem-certificate-chain; charset=utf-8"})
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/pem-certificate-chain"})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -718,7 +746,7 @@ func TestHandler_GetChallenge(t *testing.T) {
|
|||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil)
|
||||
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil, "")
|
||||
req := httptest.NewRequest("GET", u, nil)
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
@ -737,7 +765,6 @@ func TestHandler_GetChallenge(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -751,3 +778,89 @@ func TestHandler_GetChallenge(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createMetaObject(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p *provisioner.ACME
|
||||
want *Meta
|
||||
}{
|
||||
{
|
||||
name: "no-meta",
|
||||
p: &provisioner.ACME{
|
||||
Type: "ACME",
|
||||
Name: "acme",
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "terms-of-service",
|
||||
p: &provisioner.ACME{
|
||||
Type: "ACME",
|
||||
Name: "acme",
|
||||
TermsOfService: "https://terms.ca.local",
|
||||
},
|
||||
want: &Meta{
|
||||
TermsOfService: "https://terms.ca.local",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "website",
|
||||
p: &provisioner.ACME{
|
||||
Type: "ACME",
|
||||
Name: "acme",
|
||||
Website: "https://ca.local",
|
||||
},
|
||||
want: &Meta{
|
||||
Website: "https://ca.local",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "caa",
|
||||
p: &provisioner.ACME{
|
||||
Type: "ACME",
|
||||
Name: "acme",
|
||||
CaaIdentities: []string{"ca.local", "ca.remote"},
|
||||
},
|
||||
want: &Meta{
|
||||
CaaIdentities: []string{"ca.local", "ca.remote"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "require-eab",
|
||||
p: &provisioner.ACME{
|
||||
Type: "ACME",
|
||||
Name: "acme",
|
||||
RequireEAB: true,
|
||||
},
|
||||
want: &Meta{
|
||||
ExternalAccountRequired: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "full-meta",
|
||||
p: &provisioner.ACME{
|
||||
Type: "ACME",
|
||||
Name: "acme",
|
||||
TermsOfService: "https://terms.ca.local",
|
||||
Website: "https://ca.local",
|
||||
CaaIdentities: []string{"ca.local", "ca.remote"},
|
||||
RequireEAB: true,
|
||||
},
|
||||
want: &Meta{
|
||||
TermsOfService: "https://terms.ca.local",
|
||||
Website: "https://ca.local",
|
||||
CaaIdentities: []string{"ca.local", "ca.remote"},
|
||||
ExternalAccountRequired: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := createMetaObject(tt.p)
|
||||
if !cmp.Equal(tt.want, got) {
|
||||
t.Errorf("createMetaObject() diff =\n%s", cmp.Diff(tt.want, got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
|
@ -16,7 +17,6 @@ import (
|
|||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
type nextHTTP = func(http.ResponseWriter, *http.Request)
|
||||
|
@ -293,7 +293,6 @@ func lookupJWK(next nextHTTP) nextHTTP {
|
|||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
jws, err := jwsFromContext(ctx)
|
||||
if err != nil {
|
||||
|
@ -301,19 +300,16 @@ func lookupJWK(next nextHTTP) nextHTTP {
|
|||
return
|
||||
}
|
||||
|
||||
kidPrefix := linker.GetLink(ctx, acme.AccountLinkType, "")
|
||||
kid := jws.Signatures[0].Protected.KeyID
|
||||
if !strings.HasPrefix(kid, kidPrefix) {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType,
|
||||
"kid does not have required prefix; expected %s, but got %s",
|
||||
kidPrefix, kid))
|
||||
if kid == "" {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType, "signature missing 'kid'"))
|
||||
return
|
||||
}
|
||||
|
||||
accID := strings.TrimPrefix(kid, kidPrefix)
|
||||
accID := path.Base(kid)
|
||||
acc, err := db.GetAccount(ctx, accID)
|
||||
switch {
|
||||
case nosql.IsErrNotFound(err):
|
||||
case acme.IsErrNotFound(err):
|
||||
render.Error(w, acme.NewError(acme.ErrorAccountDoesNotExistType, "account with ID '%s' not found", accID))
|
||||
return
|
||||
case err != nil:
|
||||
|
@ -324,6 +320,45 @@ func lookupJWK(next nextHTTP) nextHTTP {
|
|||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account is not active"))
|
||||
return
|
||||
}
|
||||
|
||||
if storedLocation := acc.GetLocation(); storedLocation != "" {
|
||||
if kid != storedLocation {
|
||||
// ACME accounts should have a stored location equivalent to the
|
||||
// kid in the ACME request.
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
|
||||
"kid does not match stored account location; expected %s, but got %s",
|
||||
storedLocation, kid))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify that the provisioner with which the account was created
|
||||
// matches the provisioner in the request URL.
|
||||
reqProv := acme.MustProvisionerFromContext(ctx)
|
||||
reqProvName := reqProv.GetName()
|
||||
accProvName := acc.ProvisionerName
|
||||
if reqProvName != accProvName {
|
||||
// Provisioner in the URL must match the provisioner with
|
||||
// which the account was created.
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
|
||||
"account provisioner does not match requested provisioner; account provisioner = %s, requested provisioner = %s",
|
||||
accProvName, reqProvName))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// This code will only execute for old ACME accounts that do
|
||||
// not have a cached location. The following validation was
|
||||
// the original implementation of the `kid` check which has
|
||||
// since been deprecated. However, the code will remain to
|
||||
// ensure consistent behavior for old ACME accounts.
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
kidPrefix := linker.GetLink(ctx, acme.AccountLinkType, "")
|
||||
if !strings.HasPrefix(kid, kidPrefix) {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType,
|
||||
"kid does not have required prefix; expected %s, but got %s",
|
||||
kidPrefix, kid))
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, jwkContextKey, acc.Key)
|
||||
next(w, r.WithContext(ctx))
|
||||
|
|
|
@ -17,14 +17,13 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/nosql/database"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
)
|
||||
|
||||
var testBody = []byte("foo")
|
||||
|
||||
func testNext(w http.ResponseWriter, r *http.Request) {
|
||||
func testNext(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Write(testBody)
|
||||
}
|
||||
|
||||
|
@ -93,7 +92,6 @@ func TestHandler_addNonce(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -147,7 +145,6 @@ func TestHandler_addDirLink(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -252,7 +249,6 @@ func TestHandler_verifyContentType(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -320,7 +316,6 @@ func TestHandler_isPostAsGet(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -332,7 +327,7 @@ func TestHandler_isPostAsGet(t *testing.T) {
|
|||
|
||||
type errReader int
|
||||
|
||||
func (errReader) Read(p []byte) (n int, err error) {
|
||||
func (errReader) Read([]byte) (int, error) {
|
||||
return 0, errors.New("force")
|
||||
}
|
||||
func (errReader) Close() error {
|
||||
|
@ -410,7 +405,6 @@ func TestHandler_parseJWS(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -518,9 +512,6 @@ func TestHandler_verifyAndExtractJWSPayload(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"ok/empty-algorithm-in-jwk": func(t *testing.T) test {
|
||||
_pub := *pub
|
||||
clone := &_pub
|
||||
clone.Algorithm = ""
|
||||
ctx := context.WithValue(context.Background(), jwsContextKey, parsedJWS)
|
||||
ctx = context.WithValue(ctx, jwkContextKey, pub)
|
||||
return test{
|
||||
|
@ -609,7 +600,6 @@ func TestHandler_verifyAndExtractJWSPayload(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -687,31 +677,7 @@ func TestHandler_lookupJWK(t *testing.T) {
|
|||
linker: acme.NewLinker("test.ca.smallstep.com", "acme"),
|
||||
ctx: ctx,
|
||||
statusCode: 400,
|
||||
err: acme.NewError(acme.ErrorMalformedType, "kid does not have required prefix; expected %s, but got ", prefix),
|
||||
}
|
||||
},
|
||||
"fail/bad-kid-prefix": func(t *testing.T) test {
|
||||
_so := new(jose.SignerOptions)
|
||||
_so.WithHeader("kid", "foo")
|
||||
_signer, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
|
||||
Key: jwk.Key,
|
||||
}, _so)
|
||||
assert.FatalError(t, err)
|
||||
_jws, err := _signer.Sign([]byte("baz"))
|
||||
assert.FatalError(t, err)
|
||||
_raw, err := _jws.CompactSerialize()
|
||||
assert.FatalError(t, err)
|
||||
_parsed, err := jose.ParseJWS(_raw)
|
||||
assert.FatalError(t, err)
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, jwsContextKey, _parsed)
|
||||
return test{
|
||||
db: &acme.MockDB{},
|
||||
linker: acme.NewLinker("test.ca.smallstep.com", "acme"),
|
||||
ctx: ctx,
|
||||
statusCode: 400,
|
||||
err: acme.NewError(acme.ErrorMalformedType, "kid does not have required prefix; expected %s, but got foo", prefix),
|
||||
err: acme.NewError(acme.ErrorMalformedType, "signature missing 'kid'"),
|
||||
}
|
||||
},
|
||||
"fail/account-not-found": func(t *testing.T) test {
|
||||
|
@ -722,7 +688,7 @@ func TestHandler_lookupJWK(t *testing.T) {
|
|||
db: &acme.MockDB{
|
||||
MockGetAccount: func(ctx context.Context, accID string) (*acme.Account, error) {
|
||||
assert.Equals(t, accID, accID)
|
||||
return nil, database.ErrNotFound
|
||||
return nil, acme.ErrNotFound
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
|
@ -763,7 +729,77 @@ func TestHandler_lookupJWK(t *testing.T) {
|
|||
err: acme.NewError(acme.ErrorUnauthorizedType, "account is not active"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
"fail/account-with-location-prefix/bad-kid": func(t *testing.T) test {
|
||||
acc := &acme.Account{LocationPrefix: "foobar", Status: "valid"}
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
|
||||
return test{
|
||||
linker: acme.NewLinker("test.ca.smallstep.com", "acme"),
|
||||
db: &acme.MockDB{
|
||||
MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {
|
||||
assert.Equals(t, id, accID)
|
||||
return acc, nil
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: http.StatusUnauthorized,
|
||||
err: acme.NewError(acme.ErrorUnauthorizedType, "kid does not match stored account location; expected foobar, but %q", prefix+accID),
|
||||
}
|
||||
},
|
||||
"fail/account-with-location-prefix/bad-provisioner": func(t *testing.T) test {
|
||||
acc := &acme.Account{LocationPrefix: prefix + accID, Status: "valid", Key: jwk, ProvisionerName: "other"}
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
|
||||
return test{
|
||||
linker: acme.NewLinker("test.ca.smallstep.com", "acme"),
|
||||
db: &acme.MockDB{
|
||||
MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {
|
||||
assert.Equals(t, id, accID)
|
||||
return acc, nil
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
next: func(w http.ResponseWriter, r *http.Request) {
|
||||
_acc, err := accountFromContext(r.Context())
|
||||
assert.FatalError(t, err)
|
||||
assert.Equals(t, _acc, acc)
|
||||
_jwk, err := jwkFromContext(r.Context())
|
||||
assert.FatalError(t, err)
|
||||
assert.Equals(t, _jwk, jwk)
|
||||
w.Write(testBody)
|
||||
},
|
||||
statusCode: http.StatusUnauthorized,
|
||||
err: acme.NewError(acme.ErrorUnauthorizedType,
|
||||
"account provisioner does not match requested provisioner; account provisioner = %s, reqested provisioner = %s",
|
||||
prov.GetName(), "other"),
|
||||
}
|
||||
},
|
||||
"ok/account-with-location-prefix": func(t *testing.T) test {
|
||||
acc := &acme.Account{LocationPrefix: prefix + accID, Status: "valid", Key: jwk, ProvisionerName: prov.GetName()}
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
|
||||
return test{
|
||||
linker: acme.NewLinker("test.ca.smallstep.com", "acme"),
|
||||
db: &acme.MockDB{
|
||||
MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {
|
||||
assert.Equals(t, id, accID)
|
||||
return acc, nil
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
next: func(w http.ResponseWriter, r *http.Request) {
|
||||
_acc, err := accountFromContext(r.Context())
|
||||
assert.FatalError(t, err)
|
||||
assert.Equals(t, _acc, acc)
|
||||
_jwk, err := jwkFromContext(r.Context())
|
||||
assert.FatalError(t, err)
|
||||
assert.Equals(t, _jwk, jwk)
|
||||
w.Write(testBody)
|
||||
},
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
},
|
||||
"ok/account-without-location-prefix": func(t *testing.T) test {
|
||||
acc := &acme.Account{Status: "valid", Key: jwk}
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
|
||||
|
@ -811,7 +847,6 @@ func TestHandler_lookupJWK(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -1011,7 +1046,6 @@ func TestHandler_extractJWK(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -1387,7 +1421,6 @@ func TestHandler_validateJWS(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -1570,7 +1603,6 @@ func TestHandler_extractOrLookupJWK(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -1655,7 +1687,6 @@ func TestHandler_checkPrerequisites(t *testing.T) {
|
|||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
|
|
@ -392,7 +392,7 @@ func challengeTypes(az *acme.Authorization) []acme.ChallengeType {
|
|||
case acme.IP:
|
||||
chTypes = []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01}
|
||||
case acme.DNS:
|
||||
chTypes = []acme.ChallengeType{acme.DNS01}
|
||||
chTypes = []acme.ChallengeType{acme.DNS01, acme.NNS01}
|
||||
// HTTP and TLS challenges can only be used for identifiers without wildcards.
|
||||
if !az.Wildcard {
|
||||
chTypes = append(chTypes, []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01}...)
|
||||
|
|
|
@ -179,11 +179,12 @@ func TestNewOrderRequest_Validate(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.nor.Validate(); err != nil {
|
||||
if assert.NotNil(t, err) {
|
||||
ae, ok := err.(*acme.Error)
|
||||
assert.True(t, ok)
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
var ae *acme.Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
|
@ -253,11 +254,12 @@ func TestFinalizeRequestValidate(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.fr.Validate(); err != nil {
|
||||
if assert.NotNil(t, err) {
|
||||
ae, ok := err.(*acme.Error)
|
||||
assert.True(t, ok)
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
var ae *acme.Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
|
@ -484,7 +486,6 @@ func TestHandler_GetOrder(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -756,19 +757,22 @@ func TestHandler_newAuthorization(t *testing.T) {
|
|||
}
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if name == "ok/permanent-identifier-enabled" {
|
||||
println(1)
|
||||
}
|
||||
tc := run(t)
|
||||
ctx := newBaseContext(context.Background(), tc.db)
|
||||
ctx = acme.NewProvisionerContext(ctx, tc.prov)
|
||||
if err := newAuthorization(ctx, tc.az); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var k *acme.Error
|
||||
if assert.True(t, errors.As(err, &k)) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
@ -1841,7 +1845,6 @@ func TestHandler_NewOrder(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -2139,7 +2142,6 @@ func TestHandler_FinalizeOrder(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
|
|
@ -151,7 +151,7 @@ func RevokeCert(w http.ResponseWriter, r *http.Request) {
|
|||
// the identifiers in the certificate are extracted and compared against the (valid) Authorizations
|
||||
// that are stored for the ACME Account. If these sets match, the Account is considered authorized
|
||||
// to revoke the certificate. If this check fails, the client will receive an unauthorized error.
|
||||
func isAccountAuthorized(ctx context.Context, dbCert *acme.Certificate, certToBeRevoked *x509.Certificate, account *acme.Account) *acme.Error {
|
||||
func isAccountAuthorized(_ context.Context, dbCert *acme.Certificate, certToBeRevoked *x509.Certificate, account *acme.Account) *acme.Error {
|
||||
if !account.IsValid() {
|
||||
return wrapUnauthorizedError(certToBeRevoked, nil, fmt.Sprintf("account '%s' has status '%s'", account.ID, account.Status), nil)
|
||||
}
|
||||
|
|
|
@ -258,7 +258,7 @@ func jwkEncode(pub crypto.PublicKey) (string, error) {
|
|||
// jwsFinal constructs the final JWS object.
|
||||
// Implementation taken from github.com/mholt/acmez, which seems to be based on
|
||||
// https://github.com/golang/crypto/blob/master/acme/jws.go.
|
||||
func jwsFinal(sha crypto.Hash, sig []byte, phead, payload string) ([]byte, error) {
|
||||
func jwsFinal(_ crypto.Hash, sig []byte, phead, payload string) ([]byte, error) {
|
||||
enc := struct {
|
||||
Protected string `json:"protected"`
|
||||
Payload string `json:"payload"`
|
||||
|
@ -281,7 +281,7 @@ type mockCA struct {
|
|||
MockAreSANsallowed func(ctx context.Context, sans []string) error
|
||||
}
|
||||
|
||||
func (m *mockCA) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
func (m *mockCA) Sign(*x509.CertificateRequest, provisioner.SignOptions, ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -1090,7 +1090,6 @@ func TestHandler_RevokeCert(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -1230,7 +1229,6 @@ func TestHandler_isAccountAuthorized(t *testing.T) {
|
|||
assert.Equals(t, acmeErr.Type, tc.err.Type)
|
||||
assert.Equals(t, acmeErr.Status, tc.err.Status)
|
||||
assert.Equals(t, acmeErr.Detail, tc.err.Detail)
|
||||
assert.Equals(t, acmeErr.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, acmeErr.Subproblems, tc.err.Subproblems)
|
||||
|
||||
})
|
||||
|
@ -1323,7 +1321,6 @@ func Test_wrapUnauthorizedError(t *testing.T) {
|
|||
assert.Equals(t, acmeErr.Type, tc.want.Type)
|
||||
assert.Equals(t, acmeErr.Status, tc.want.Status)
|
||||
assert.Equals(t, acmeErr.Detail, tc.want.Detail)
|
||||
assert.Equals(t, acmeErr.Identifier, tc.want.Identifier)
|
||||
assert.Equals(t, acmeErr.Subproblems, tc.want.Subproblems)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,15 +8,16 @@ import (
|
|||
|
||||
// Authorization representst an ACME Authorization.
|
||||
type Authorization struct {
|
||||
ID string `json:"-"`
|
||||
AccountID string `json:"-"`
|
||||
Token string `json:"-"`
|
||||
Identifier Identifier `json:"identifier"`
|
||||
Status Status `json:"status"`
|
||||
Challenges []*Challenge `json:"challenges"`
|
||||
Wildcard bool `json:"wildcard"`
|
||||
ExpiresAt time.Time `json:"expires"`
|
||||
Error *Error `json:"error,omitempty"`
|
||||
ID string `json:"-"`
|
||||
AccountID string `json:"-"`
|
||||
Token string `json:"-"`
|
||||
Fingerprint string `json:"-"`
|
||||
Identifier Identifier `json:"identifier"`
|
||||
Status Status `json:"status"`
|
||||
Challenges []*Challenge `json:"challenges"`
|
||||
Wildcard bool `json:"wildcard"`
|
||||
ExpiresAt time.Time `json:"expires"`
|
||||
Error *Error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ToLog enables response logging.
|
||||
|
|
|
@ -130,14 +130,14 @@ func TestAuthorization_UpdateStatus(t *testing.T) {
|
|||
tc := run(t)
|
||||
if err := tc.az.UpdateStatus(context.Background(), tc.db); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,9 +26,16 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/smallstep/go-attestation/attest"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
)
|
||||
|
||||
type ChallengeType string
|
||||
|
@ -42,6 +49,20 @@ const (
|
|||
TLSALPN01 ChallengeType = "tls-alpn-01"
|
||||
// DEVICEATTEST01 is the device-attest-01 ACME challenge type
|
||||
DEVICEATTEST01 ChallengeType = "device-attest-01"
|
||||
// NNS01 is the nns-01 ACME challenge type
|
||||
NNS01 ChallengeType = "nns-01"
|
||||
)
|
||||
|
||||
var (
|
||||
// InsecurePortHTTP01 is the port used to verify http-01 challenges. If not set it
|
||||
// defaults to 80.
|
||||
InsecurePortHTTP01 int
|
||||
|
||||
// InsecurePortTLSALPN01 is the port used to verify tls-alpn-01 challenges. If not
|
||||
// set it defaults to 443.
|
||||
//
|
||||
// This variable can be used for testing purposes.
|
||||
InsecurePortTLSALPN01 int
|
||||
)
|
||||
|
||||
// Challenge represents an ACME response Challenge type.
|
||||
|
@ -67,10 +88,9 @@ func (ch *Challenge) ToLog() (interface{}, error) {
|
|||
return string(b), nil
|
||||
}
|
||||
|
||||
// Validate attempts to validate the challenge. Stores changes to the Challenge
|
||||
// type using the DB interface.
|
||||
// satisfactorily validated, the 'status' and 'validated' attributes are
|
||||
// updated.
|
||||
// Validate attempts to validate the Challenge. Stores changes to the Challenge
|
||||
// type using the DB interface. If the Challenge is validated, the 'status' and
|
||||
// 'validated' attributes are updated.
|
||||
func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, payload []byte) error {
|
||||
// If already valid or invalid then return without performing validation.
|
||||
if ch.Status != StatusPending {
|
||||
|
@ -85,6 +105,8 @@ func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey,
|
|||
return tlsalpn01Validate(ctx, ch, db, jwk)
|
||||
case DEVICEATTEST01:
|
||||
return deviceAttest01Validate(ctx, ch, db, jwk, payload)
|
||||
case NNS01:
|
||||
return nns01Validate(ctx, ch, db, jwk)
|
||||
default:
|
||||
return NewErrorISE("unexpected challenge type '%s'", ch.Type)
|
||||
}
|
||||
|
@ -93,6 +115,12 @@ func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey,
|
|||
func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error {
|
||||
u := &url.URL{Scheme: "http", Host: http01ChallengeHost(ch.Value), Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)}
|
||||
|
||||
// Append insecure port if set.
|
||||
// Only used for testing purposes.
|
||||
if InsecurePortHTTP01 != 0 {
|
||||
u.Host += ":" + strconv.Itoa(InsecurePortHTTP01)
|
||||
}
|
||||
|
||||
vc := MustClientFromContext(ctx)
|
||||
resp, err := vc.Get(u.String())
|
||||
if err != nil {
|
||||
|
@ -162,10 +190,17 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON
|
|||
// [RFC5246] or higher when connecting to clients for validation.
|
||||
MinVersion: tls.VersionTLS12,
|
||||
ServerName: serverName(ch),
|
||||
InsecureSkipVerify: true, // nolint:gosec // we expect a self-signed challenge certificate
|
||||
InsecureSkipVerify: true, //nolint:gosec // we expect a self-signed challenge certificate
|
||||
}
|
||||
|
||||
hostPort := net.JoinHostPort(ch.Value, "443")
|
||||
var hostPort string
|
||||
|
||||
// Allow to change TLS port for testing purposes.
|
||||
if port := InsecurePortTLSALPN01; port == 0 {
|
||||
hostPort = net.JoinHostPort(ch.Value, "443")
|
||||
} else {
|
||||
hostPort = net.JoinHostPort(ch.Value, strconv.Itoa(port))
|
||||
}
|
||||
|
||||
vc := MustClientFromContext(ctx)
|
||||
conn, err := vc.TLSDial("tcp", hostPort, config)
|
||||
|
@ -310,20 +345,26 @@ func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebK
|
|||
return nil
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
type payloadType struct {
|
||||
AttObj string `json:"attObj"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type AttestationObject struct {
|
||||
type attestationObject struct {
|
||||
Format string `json:"fmt"`
|
||||
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
||||
}
|
||||
|
||||
// TODO(bweeks): move attestation verification to a shared package.
|
||||
// TODO(bweeks): define new error type for failed attestation validation.
|
||||
func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error {
|
||||
var p Payload
|
||||
// Load authorization to store the key fingerprint.
|
||||
az, err := db.GetAuthorization(ctx, ch.AuthorizationID)
|
||||
if err != nil {
|
||||
return WrapErrorISE(err, "error loading authorization")
|
||||
}
|
||||
|
||||
// Parse payload.
|
||||
var p payloadType
|
||||
if err := json.Unmarshal(payload, &p); err != nil {
|
||||
return WrapErrorISE(err, "error unmarshalling JSON")
|
||||
}
|
||||
|
@ -337,7 +378,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
|
|||
return WrapErrorISE(err, "error base64 decoding attObj")
|
||||
}
|
||||
|
||||
att := AttestationObject{}
|
||||
att := attestationObject{}
|
||||
if err := cbor.Unmarshal(attObj, &att); err != nil {
|
||||
return WrapErrorISE(err, "error unmarshalling CBOR")
|
||||
}
|
||||
|
@ -361,7 +402,6 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
|
|||
}
|
||||
return WrapErrorISE(err, "error validating attestation")
|
||||
}
|
||||
|
||||
// Validate nonce with SHA-256 of the token.
|
||||
if len(data.Nonce) != 0 {
|
||||
sum := sha256.Sum256([]byte(ch.Token))
|
||||
|
@ -377,6 +417,9 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
|
|||
if data.UDID != ch.Value && data.SerialNumber != ch.Value {
|
||||
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match"))
|
||||
}
|
||||
|
||||
// Update attestation key fingerprint to compare against the CSR
|
||||
az.Fingerprint = data.Fingerprint
|
||||
case "step":
|
||||
data, err := doStepAttestationFormat(ctx, prov, ch, jwk, &att)
|
||||
if err != nil {
|
||||
|
@ -390,13 +433,53 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
|
|||
return WrapErrorISE(err, "error validating attestation")
|
||||
}
|
||||
|
||||
// Validate Apple's ClientIdentifier (Identifier.Value) with device
|
||||
// identifiers.
|
||||
// Validate the YubiKey serial number from the attestation
|
||||
// certificate with the challenged Order value.
|
||||
//
|
||||
// Note: We might want to use an external service for this.
|
||||
if data.SerialNumber != ch.Value {
|
||||
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match"))
|
||||
subproblem := NewSubproblemWithIdentifier(
|
||||
ErrorMalformedType,
|
||||
Identifier{Type: "permanent-identifier", Value: ch.Value},
|
||||
"challenge identifier %q doesn't match the attested hardware identifier %q", ch.Value, data.SerialNumber,
|
||||
)
|
||||
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match").AddSubproblems(subproblem))
|
||||
}
|
||||
|
||||
// Update attestation key fingerprint to compare against the CSR
|
||||
az.Fingerprint = data.Fingerprint
|
||||
|
||||
case "tpm":
|
||||
data, err := doTPMAttestationFormat(ctx, prov, ch, jwk, &att)
|
||||
if err != nil {
|
||||
// TODO(hs): we should provide more details in the error reported to the client;
|
||||
// "Attestation statement cannot be verified" is VERY generic. Also holds true for the other formats.
|
||||
var acmeError *Error
|
||||
if errors.As(err, &acmeError) {
|
||||
if acmeError.Status == 500 {
|
||||
return acmeError
|
||||
}
|
||||
return storeError(ctx, db, ch, true, acmeError)
|
||||
}
|
||||
return WrapErrorISE(err, "error validating attestation")
|
||||
}
|
||||
|
||||
// TODO(hs): currently this will allow a request for which no PermanentIdentifiers have been
|
||||
// extracted from the AK certificate. This is currently the case for AK certs from the CLI, as we
|
||||
// haven't implemented a way for AK certs requested by the CLI to always contain the requested
|
||||
// PermanentIdentifier. Omitting the check below doesn't allow just any request, as the Order can
|
||||
// still fail if the challenge value isn't equal to the CSR subject.
|
||||
if len(data.PermanentIdentifiers) > 0 && !slices.Contains(data.PermanentIdentifiers, ch.Value) { // TODO(hs): add support for HardwareModuleName
|
||||
subproblem := NewSubproblemWithIdentifier(
|
||||
ErrorMalformedType,
|
||||
Identifier{Type: "permanent-identifier", Value: ch.Value},
|
||||
"challenge identifier %q doesn't match any of the attested hardware identifiers %q", ch.Value, data.PermanentIdentifiers,
|
||||
)
|
||||
return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, "permanent identifier does not match").AddSubproblems(subproblem))
|
||||
}
|
||||
|
||||
// Update attestation key fingerprint to compare against the CSR
|
||||
az.Fingerprint = data.Fingerprint
|
||||
default:
|
||||
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "unexpected attestation object format"))
|
||||
}
|
||||
|
@ -406,12 +489,362 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
|
|||
ch.Error = nil
|
||||
ch.ValidatedAt = clock.Now().Format(time.RFC3339)
|
||||
|
||||
// Store the fingerprint in the authorization.
|
||||
//
|
||||
// TODO: add method to update authorization and challenge atomically.
|
||||
if az.Fingerprint != "" {
|
||||
if err := db.UpdateAuthorization(ctx, az); err != nil {
|
||||
return WrapErrorISE(err, "error updating authorization")
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.UpdateChallenge(ctx, ch); err != nil {
|
||||
return WrapErrorISE(err, "error updating challenge")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func nns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error {
|
||||
domain := strings.TrimPrefix(ch.Value, "*.")
|
||||
|
||||
nnsCtx, ok := GetNNSContext(ctx)
|
||||
if !ok {
|
||||
return errors.New("error retrieving NNS context")
|
||||
}
|
||||
|
||||
nns := NNS{}
|
||||
err := nns.Dial(nnsCtx.nnsServer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer nns.Close()
|
||||
|
||||
txtRecords, err := nns.GetTXTRecords("acme-challenge." + domain)
|
||||
if err != nil {
|
||||
return storeError(ctx, db, ch, false, WrapError(ErrorNNSType, err,
|
||||
"error looking up TXT records for domain %s", domain))
|
||||
}
|
||||
|
||||
expectedKeyAuth, err := KeyAuthorization(ch.Token, jwk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h := sha256.Sum256([]byte(expectedKeyAuth))
|
||||
expected := base64.RawURLEncoding.EncodeToString(h[:])
|
||||
var found bool
|
||||
for _, r := range txtRecords {
|
||||
if r == expected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return storeError(ctx, db, ch, false, NewError(ErrorRejectedIdentifierType,
|
||||
"keyAuthorization does not match; expected %s, but got %s", expectedKeyAuth, txtRecords))
|
||||
}
|
||||
|
||||
// Update and store the challenge.
|
||||
ch.Status = StatusValid
|
||||
ch.Error = nil
|
||||
ch.ValidatedAt = clock.Now().Format(time.RFC3339)
|
||||
|
||||
if err = db.UpdateChallenge(ctx, ch); err != nil {
|
||||
return WrapErrorISE(err, "error updating challenge")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
oidSubjectAlternativeName = asn1.ObjectIdentifier{2, 5, 29, 17}
|
||||
)
|
||||
|
||||
type tpmAttestationData struct {
|
||||
Certificate *x509.Certificate
|
||||
VerifiedChains [][]*x509.Certificate
|
||||
PermanentIdentifiers []string
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
// coseAlgorithmIdentifier models a COSEAlgorithmIdentifier.
|
||||
// Also see https://www.w3.org/TR/webauthn-2/#sctn-alg-identifier.
|
||||
type coseAlgorithmIdentifier int32
|
||||
|
||||
const (
|
||||
coseAlgES256 coseAlgorithmIdentifier = -7
|
||||
coseAlgRS256 coseAlgorithmIdentifier = -257
|
||||
)
|
||||
|
||||
func doTPMAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*tpmAttestationData, error) {
|
||||
ver, ok := att.AttStatement["ver"].(string)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "ver not present")
|
||||
}
|
||||
if ver != "2.0" {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "version %q is not supported", ver)
|
||||
}
|
||||
|
||||
x5c, ok := att.AttStatement["x5c"].([]interface{})
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "x5c not present")
|
||||
}
|
||||
if len(x5c) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "x5c is empty")
|
||||
}
|
||||
|
||||
akCertBytes, ok := x5c[0].([]byte)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed")
|
||||
}
|
||||
akCert, err := x509.ParseCertificate(akCertBytes)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed")
|
||||
}
|
||||
|
||||
intermediates := x509.NewCertPool()
|
||||
for _, v := range x5c[1:] {
|
||||
intCertBytes, vok := v.([]byte)
|
||||
if !vok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed")
|
||||
}
|
||||
intCert, err := x509.ParseCertificate(intCertBytes)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed")
|
||||
}
|
||||
intermediates.AddCert(intCert)
|
||||
}
|
||||
|
||||
// TODO(hs): this can be removed when permanent-identifier/hardware-module-name are handled correctly in
|
||||
// the stdlib in https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/crypto/x509/parser.go;drc=b5b2cf519fe332891c165077f3723ee74932a647;l=362,
|
||||
// but I doubt that will happen.
|
||||
if len(akCert.UnhandledCriticalExtensions) > 0 {
|
||||
unhandledCriticalExtensions := akCert.UnhandledCriticalExtensions[:0]
|
||||
for _, extOID := range akCert.UnhandledCriticalExtensions {
|
||||
if !extOID.Equal(oidSubjectAlternativeName) {
|
||||
// critical extensions other than the Subject Alternative Name remain unhandled
|
||||
unhandledCriticalExtensions = append(unhandledCriticalExtensions, extOID)
|
||||
}
|
||||
}
|
||||
akCert.UnhandledCriticalExtensions = unhandledCriticalExtensions
|
||||
}
|
||||
|
||||
roots, ok := prov.GetAttestationRoots()
|
||||
if !ok {
|
||||
return nil, NewErrorISE("no root CA bundle available to verify the attestation certificate")
|
||||
}
|
||||
|
||||
// verify that the AK certificate was signed by a trusted root,
|
||||
// chained to by the intermediates provided by the client. As part
|
||||
// of building the verified certificate chain, the signature over the
|
||||
// AK certificate is checked to be a valid signature of one of the
|
||||
// provided intermediates. Signatures over the intermediates are in
|
||||
// turn also verified to be valid signatures from one of the trusted
|
||||
// roots.
|
||||
verifiedChains, err := akCert.Verify(x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
Intermediates: intermediates,
|
||||
CurrentTime: time.Now().Truncate(time.Second),
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is not valid")
|
||||
}
|
||||
|
||||
// validate additional AK certificate requirements
|
||||
if err := validateAKCertificate(akCert); err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "AK certificate is not valid")
|
||||
}
|
||||
|
||||
// TODO(hs): implement revocation check; Verify() doesn't perform CRL check nor OCSP lookup.
|
||||
|
||||
sans, err := x509util.ParseSubjectAlternativeNames(akCert)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed parsing AK certificate Subject Alternative Names")
|
||||
}
|
||||
|
||||
permanentIdentifiers := make([]string, len(sans.PermanentIdentifiers))
|
||||
for i, pi := range sans.PermanentIdentifiers {
|
||||
permanentIdentifiers[i] = pi.Identifier
|
||||
}
|
||||
|
||||
// extract and validate pubArea, sig, certInfo and alg properties from the request body
|
||||
pubArea, ok := att.AttStatement["pubArea"].([]byte)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid pubArea in attestation statement")
|
||||
}
|
||||
if len(pubArea) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "pubArea is empty")
|
||||
}
|
||||
|
||||
sig, ok := att.AttStatement["sig"].([]byte)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid sig in attestation statement")
|
||||
}
|
||||
if len(sig) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "sig is empty")
|
||||
}
|
||||
|
||||
certInfo, ok := att.AttStatement["certInfo"].([]byte)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid certInfo in attestation statement")
|
||||
}
|
||||
if len(certInfo) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "certInfo is empty")
|
||||
}
|
||||
|
||||
alg, ok := att.AttStatement["alg"].(int64)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid alg in attestation statement")
|
||||
}
|
||||
|
||||
// only RS256 and ES256 are allowed
|
||||
coseAlg := coseAlgorithmIdentifier(alg)
|
||||
if coseAlg != coseAlgRS256 && coseAlg != coseAlgES256 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid alg %d in attestation statement", alg)
|
||||
}
|
||||
|
||||
// set the hash algorithm to use to SHA256
|
||||
hash := crypto.SHA256
|
||||
|
||||
// recreate the generated key certification parameter values and verify
|
||||
// the attested key using the public key of the AK.
|
||||
certificationParameters := &attest.CertificationParameters{
|
||||
Public: pubArea, // the public key that was attested
|
||||
CreateAttestation: certInfo, // the attested properties of the key
|
||||
CreateSignature: sig, // signature over the attested properties
|
||||
}
|
||||
verifyOpts := attest.VerifyOpts{
|
||||
Public: akCert.PublicKey, // public key of the AK that attested the key
|
||||
Hash: hash,
|
||||
}
|
||||
if err = certificationParameters.Verify(verifyOpts); err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "invalid certification parameters")
|
||||
}
|
||||
|
||||
// decode the "certInfo" data. This won't fail, as it's also done as part of Verify().
|
||||
tpmCertInfo, err := tpm2.DecodeAttestationData(certInfo)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed decoding attestation data")
|
||||
}
|
||||
|
||||
keyAuth, err := KeyAuthorization(ch.Token, jwk)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed creating key auth digest")
|
||||
}
|
||||
hashedKeyAuth := sha256.Sum256([]byte(keyAuth))
|
||||
|
||||
// verify the WebAuthn object contains the expect key authorization digest, which is carried
|
||||
// within the encoded `certInfo` property of the attestation statement.
|
||||
if subtle.ConstantTimeCompare(hashedKeyAuth[:], []byte(tpmCertInfo.ExtraData)) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "key authorization does not match")
|
||||
}
|
||||
|
||||
// decode the (attested) public key and determine its fingerprint. This won't fail, as it's also done as part of Verify().
|
||||
pub, err := tpm2.DecodePublic(pubArea)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed decoding pubArea")
|
||||
}
|
||||
|
||||
publicKey, err := pub.Key()
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed getting public key")
|
||||
}
|
||||
|
||||
data := &tpmAttestationData{
|
||||
Certificate: akCert,
|
||||
VerifiedChains: verifiedChains,
|
||||
PermanentIdentifiers: permanentIdentifiers,
|
||||
}
|
||||
|
||||
if data.Fingerprint, err = keyutil.Fingerprint(publicKey); err != nil {
|
||||
return nil, WrapErrorISE(err, "error calculating key fingerprint")
|
||||
}
|
||||
|
||||
// TODO(hs): pass more attestation data, so that that can be used/recorded too?
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var (
|
||||
oidExtensionExtendedKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37}
|
||||
oidTCGKpAIKCertificate = asn1.ObjectIdentifier{2, 23, 133, 8, 3}
|
||||
)
|
||||
|
||||
// validateAKCertifiate validates the X.509 AK certificate to be
|
||||
// in accordance with the required properties. The requirements come from:
|
||||
// https://www.w3.org/TR/webauthn-2/#sctn-tpm-cert-requirements.
|
||||
//
|
||||
// - Version MUST be set to 3.
|
||||
// - Subject field MUST be set to empty.
|
||||
// - The Subject Alternative Name extension MUST be set as defined
|
||||
// in [TPMv2-EK-Profile] section 3.2.9.
|
||||
// - The Extended Key Usage extension MUST contain the OID 2.23.133.8.3
|
||||
// ("joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)").
|
||||
// - The Basic Constraints extension MUST have the CA component set to false.
|
||||
// - An Authority Information Access (AIA) extension with entry id-ad-ocsp
|
||||
// and a CRL Distribution Point extension [RFC5280] are both OPTIONAL as
|
||||
// the status of many attestation certificates is available through metadata
|
||||
// services. See, for example, the FIDO Metadata Service.
|
||||
func validateAKCertificate(c *x509.Certificate) error {
|
||||
if c.Version != 3 {
|
||||
return fmt.Errorf("AK certificate has invalid version %d; only version 3 is allowed", c.Version)
|
||||
}
|
||||
if c.Subject.String() != "" {
|
||||
return fmt.Errorf("AK certificate subject must be empty; got %q", c.Subject)
|
||||
}
|
||||
if c.IsCA {
|
||||
return errors.New("AK certificate must not be a CA")
|
||||
}
|
||||
if err := validateAKCertificateExtendedKeyUsage(c); err != nil {
|
||||
return err
|
||||
}
|
||||
return validateAKCertificateSubjectAlternativeNames(c)
|
||||
}
|
||||
|
||||
// validateAKCertificateSubjectAlternativeNames checks if the AK certificate
|
||||
// has TPM hardware details set.
|
||||
func validateAKCertificateSubjectAlternativeNames(c *x509.Certificate) error {
|
||||
sans, err := x509util.ParseSubjectAlternativeNames(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing AK certificate Subject Alternative Names: %w", err)
|
||||
}
|
||||
|
||||
details := sans.TPMHardwareDetails
|
||||
manufacturer, model, version := details.Manufacturer, details.Model, details.Version
|
||||
|
||||
switch {
|
||||
case manufacturer == "":
|
||||
return errors.New("missing TPM manufacturer")
|
||||
case model == "":
|
||||
return errors.New("missing TPM model")
|
||||
case version == "":
|
||||
return errors.New("missing TPM version")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAKCertificateExtendedKeyUsage checks if the AK certificate
|
||||
// has the "tcg-kp-AIKCertificate" Extended Key Usage set.
|
||||
func validateAKCertificateExtendedKeyUsage(c *x509.Certificate) error {
|
||||
var (
|
||||
valid = false
|
||||
ekus []asn1.ObjectIdentifier
|
||||
)
|
||||
for _, ext := range c.Extensions {
|
||||
if ext.Id.Equal(oidExtensionExtendedKeyUsage) {
|
||||
if _, err := asn1.Unmarshal(ext.Value, &ekus); err != nil || !ekus[0].Equal(oidTCGKpAIKCertificate) {
|
||||
return errors.New("AK certificate is missing Extended Key Usage value tcg-kp-AIKCertificate (2.23.133.8.3)")
|
||||
}
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return errors.New("AK certificate is missing Extended Key Usage extension")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apple Enterprise Attestation Root CA from
|
||||
// https://www.apple.com/certificateauthority/private/
|
||||
const appleEnterpriseAttestationRootCA = `-----BEGIN CERTIFICATE-----
|
||||
|
@ -442,9 +875,10 @@ type appleAttestationData struct {
|
|||
UDID string
|
||||
SEPVersion string
|
||||
Certificate *x509.Certificate
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, att *AttestationObject) (*appleAttestationData, error) {
|
||||
func doAppleAttestationFormat(_ context.Context, prov Provisioner, _ *Challenge, att *attestationObject) (*appleAttestationData, error) {
|
||||
// Use configured or default attestation roots if none is configured.
|
||||
roots, ok := prov.GetAttestationRoots()
|
||||
if !ok {
|
||||
|
@ -498,6 +932,9 @@ func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challen
|
|||
data := &appleAttestationData{
|
||||
Certificate: leaf,
|
||||
}
|
||||
if data.Fingerprint, err = keyutil.Fingerprint(leaf.PublicKey); err != nil {
|
||||
return nil, WrapErrorISE(err, "error calculating key fingerprint")
|
||||
}
|
||||
for _, ext := range leaf.Extensions {
|
||||
switch {
|
||||
case ext.Id.Equal(oidAppleSerialNumber):
|
||||
|
@ -543,9 +980,10 @@ var oidYubicoSerialNumber = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 41482, 3, 7}
|
|||
type stepAttestationData struct {
|
||||
Certificate *x509.Certificate
|
||||
SerialNumber string
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *AttestationObject) (*stepAttestationData, error) {
|
||||
func doStepAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*stepAttestationData, error) {
|
||||
// Use configured or default attestation roots if none is configured.
|
||||
roots, ok := prov.GetAttestationRoots()
|
||||
if !ok {
|
||||
|
@ -638,6 +1076,9 @@ func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challeng
|
|||
data := &stepAttestationData{
|
||||
Certificate: leaf,
|
||||
}
|
||||
if data.Fingerprint, err = keyutil.Fingerprint(leaf.PublicKey); err != nil {
|
||||
return nil, WrapErrorISE(err, "error calculating key fingerprint")
|
||||
}
|
||||
for _, ext := range leaf.Extensions {
|
||||
if !ext.Id.Equal(oidYubicoSerialNumber) {
|
||||
continue
|
||||
|
@ -701,10 +1142,10 @@ func uitoa(val uint) string {
|
|||
var buf [20]byte // big enough for 64bit value base 10
|
||||
i := len(buf) - 1
|
||||
for val >= 10 {
|
||||
q := val / 10
|
||||
buf[i] = byte('0' + val - q*10)
|
||||
v := val / 10
|
||||
buf[i] = byte('0' + val - v*10)
|
||||
i--
|
||||
val = q
|
||||
val = v
|
||||
}
|
||||
// val < 10
|
||||
buf[i] = byte('0' + val)
|
||||
|
|
File diff suppressed because it is too large
Load diff
860
acme/challenge_tpmsimulator_test.go
Normal file
860
acme/challenge_tpmsimulator_test.go
Normal file
|
@ -0,0 +1,860 @@
|
|||
//go:build tpmsimulator
|
||||
// +build tpmsimulator
|
||||
|
||||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/smallstep/go-attestation/attest"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/minica"
|
||||
"go.step.sm/crypto/tpm"
|
||||
"go.step.sm/crypto/tpm/simulator"
|
||||
tpmstorage "go.step.sm/crypto/tpm/storage"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
func newSimulatedTPM(t *testing.T) *tpm.TPM {
|
||||
t.Helper()
|
||||
tmpDir := t.TempDir()
|
||||
tpm, err := tpm.New(withSimulator(t), tpm.WithStore(tpmstorage.NewDirstore(tmpDir))) // TODO: provide in-memory storage implementation instead
|
||||
require.NoError(t, err)
|
||||
return tpm
|
||||
}
|
||||
|
||||
func withSimulator(t *testing.T) tpm.NewTPMOption {
|
||||
t.Helper()
|
||||
var sim simulator.Simulator
|
||||
t.Cleanup(func() {
|
||||
if sim == nil {
|
||||
return
|
||||
}
|
||||
err := sim.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
sim, err := simulator.New()
|
||||
require.NoError(t, err)
|
||||
err = sim.Open()
|
||||
require.NoError(t, err)
|
||||
return tpm.WithSimulator(sim)
|
||||
}
|
||||
|
||||
func generateKeyID(t *testing.T, pub crypto.PublicKey) []byte {
|
||||
t.Helper()
|
||||
b, err := x509.MarshalPKIXPublicKey(pub)
|
||||
require.NoError(t, err)
|
||||
hash := sha256.Sum256(b)
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
func mustAttestTPM(t *testing.T, keyAuthorization string, permanentIdentifiers []string) ([]byte, crypto.Signer, *x509.Certificate) {
|
||||
t.Helper()
|
||||
aca, err := minica.New(
|
||||
minica.WithName("TPM Testing"),
|
||||
minica.WithGetSignerFunc(
|
||||
func() (crypto.Signer, error) {
|
||||
return keyutil.GenerateSigner("RSA", "", 2048)
|
||||
},
|
||||
),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// prepare simulated TPM and create an AK
|
||||
stpm := newSimulatedTPM(t)
|
||||
eks, err := stpm.GetEKs(context.Background())
|
||||
require.NoError(t, err)
|
||||
ak, err := stpm.CreateAK(context.Background(), "first-ak")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ak)
|
||||
|
||||
// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())
|
||||
ap, err := ak.AttestationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
akp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create template and sign certificate for the AK public key
|
||||
keyID := generateKeyID(t, eks[0].Public())
|
||||
template := &x509.Certificate{
|
||||
PublicKey: akp.Public,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
sans := []x509util.SubjectAlternativeName{}
|
||||
uris := []*url.URL{{Scheme: "urn", Opaque: "ek:sha256:" + base64.StdEncoding.EncodeToString(keyID)}}
|
||||
for _, pi := range permanentIdentifiers {
|
||||
sans = append(sans, x509util.SubjectAlternativeName{
|
||||
Type: x509util.PermanentIdentifierType,
|
||||
Value: pi,
|
||||
})
|
||||
}
|
||||
asn1Value := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMModel, "SLB 9670 TPM2.0", oidTPMVersion, "7.55"))
|
||||
sans = append(sans, x509util.SubjectAlternativeName{
|
||||
Type: x509util.DirectoryNameType,
|
||||
ASN1Value: asn1Value,
|
||||
})
|
||||
ext, err := createSubjectAltNameExtension(nil, nil, nil, uris, sans, true)
|
||||
require.NoError(t, err)
|
||||
ext.Set(template)
|
||||
akCert, err := aca.Sign(template)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, akCert)
|
||||
|
||||
// create a new key attested by the AK, while including
|
||||
// the key authorization bytes as qualifying data.
|
||||
keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
|
||||
config := tpm.AttestKeyConfig{
|
||||
Algorithm: "RSA",
|
||||
Size: 2048,
|
||||
QualifyingData: keyAuthSum[:],
|
||||
}
|
||||
key, err := stpm.AttestKey(context.Background(), "first-ak", "first-key", config)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
require.Equal(t, "first-key", key.Name())
|
||||
require.NotEqual(t, 0, len(key.Data()))
|
||||
require.Equal(t, "first-ak", key.AttestedBy())
|
||||
require.True(t, key.WasAttested())
|
||||
require.True(t, key.WasAttestedBy(ak))
|
||||
|
||||
signer, err := key.Signer(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// prepare the attestation object with the AK certificate chain,
|
||||
// the attested key, its metadata and the signature signed by the
|
||||
// AK.
|
||||
params, err := key.CertificationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
attObj, err := cbor.Marshal(struct {
|
||||
Format string `json:"fmt"`
|
||||
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
||||
}{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// marshal the ACME payload
|
||||
payload, err := json.Marshal(struct {
|
||||
AttObj string `json:"attObj"`
|
||||
}{
|
||||
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return payload, signer, aca.Root
|
||||
}
|
||||
|
||||
func Test_deviceAttest01ValidateWithTPMSimulator(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
ch *Challenge
|
||||
db DB
|
||||
jwk *jose.JSONWebKey
|
||||
payload []byte
|
||||
}
|
||||
type test struct {
|
||||
args args
|
||||
wantErr *Error
|
||||
}
|
||||
tests := map[string]func(t *testing.T) test{
|
||||
"ok/doTPMAttestationFormat-storeError": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, _, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
|
||||
// parse payload, set invalid "ver", remarshal
|
||||
var p payloadType
|
||||
err := json.Unmarshal(payload, &p)
|
||||
require.NoError(t, err)
|
||||
attObj, err := base64.RawURLEncoding.DecodeString(p.AttObj)
|
||||
require.NoError(t, err)
|
||||
att := attestationObject{}
|
||||
err = cbor.Unmarshal(attObj, &att)
|
||||
require.NoError(t, err)
|
||||
att.AttStatement["ver"] = "bogus"
|
||||
attObj, err = cbor.Marshal(struct {
|
||||
Format string `json:"fmt"`
|
||||
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
||||
}{
|
||||
Format: "tpm",
|
||||
AttStatement: att.AttStatement,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
payload, err = json.Marshal(struct {
|
||||
AttObj string `json:"attObj"`
|
||||
}{
|
||||
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.12345678",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusInvalid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.12345678", updch.Value)
|
||||
|
||||
err := NewError(ErrorBadAttestationStatementType, `version "bogus" is not supported`)
|
||||
|
||||
assert.EqualError(t, updch.Error.Err, err.Err.Error())
|
||||
assert.Equal(t, err.Type, updch.Error.Type)
|
||||
assert.Equal(t, err.Detail, updch.Error.Detail)
|
||||
assert.Equal(t, err.Status, updch.Error.Status)
|
||||
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
"ok with invalid PermanentIdentifier SAN": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, _, root := mustAttestTPM(t, keyAuth, []string{"device.id.12345678"}) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.99999999",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusInvalid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.99999999", updch.Value)
|
||||
|
||||
err := NewError(ErrorRejectedIdentifierType, `permanent identifier does not match`).
|
||||
AddSubproblems(NewSubproblemWithIdentifier(
|
||||
ErrorMalformedType,
|
||||
Identifier{Type: "permanent-identifier", Value: "device.id.99999999"},
|
||||
`challenge identifier "device.id.99999999" doesn't match any of the attested hardware identifiers ["device.id.12345678"]`,
|
||||
))
|
||||
|
||||
assert.EqualError(t, updch.Error.Err, err.Err.Error())
|
||||
assert.Equal(t, err.Type, updch.Error.Type)
|
||||
assert.Equal(t, err.Detail, updch.Error.Detail)
|
||||
assert.Equal(t, err.Status, updch.Error.Status)
|
||||
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, signer, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.12345678",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "azID", az.ID)
|
||||
assert.Equal(t, fingerprint, az.Fingerprint)
|
||||
return nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusValid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.12345678", updch.Value)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
"ok with PermanentIdentifier SAN": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, signer, root := mustAttestTPM(t, keyAuth, []string{"device.id.12345678"}) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.12345678",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "azID", az.ID)
|
||||
assert.Equal(t, fingerprint, az.Fingerprint)
|
||||
return nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusValid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.12345678", updch.Value)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := run(t)
|
||||
|
||||
if err := deviceAttest01Validate(tc.args.ctx, tc.args.ch, tc.args.db, tc.args.jwk, tc.args.payload); err != nil {
|
||||
assert.Error(t, tc.wantErr)
|
||||
assert.EqualError(t, err, tc.wantErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.Nil(t, tc.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newBadAttestationStatementError(msg string) *Error {
|
||||
return &Error{
|
||||
Type: "urn:ietf:params:acme:error:badAttestationStatement",
|
||||
Status: 400,
|
||||
Err: errors.New(msg),
|
||||
}
|
||||
}
|
||||
|
||||
func newInternalServerError(msg string) *Error {
|
||||
return &Error{
|
||||
Type: "urn:ietf:params:acme:error:serverInternal",
|
||||
Status: 500,
|
||||
Err: errors.New(msg),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
oidPermanentIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}
|
||||
oidHardwareModuleNameIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 4}
|
||||
)
|
||||
|
||||
func Test_doTPMAttestationFormat(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
aca, err := minica.New(
|
||||
minica.WithName("TPM Testing"),
|
||||
minica.WithGetSignerFunc(
|
||||
func() (crypto.Signer, error) {
|
||||
return keyutil.GenerateSigner("RSA", "", 2048)
|
||||
},
|
||||
),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
acaRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: aca.Root.Raw})
|
||||
|
||||
// prepare simulated TPM and create an AK
|
||||
stpm := newSimulatedTPM(t)
|
||||
eks, err := stpm.GetEKs(context.Background())
|
||||
require.NoError(t, err)
|
||||
ak, err := stpm.CreateAK(context.Background(), "first-ak")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ak)
|
||||
|
||||
// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())
|
||||
ap, err := ak.AttestationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
akp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create template and sign certificate for the AK public key
|
||||
keyID := generateKeyID(t, eks[0].Public())
|
||||
template := &x509.Certificate{
|
||||
PublicKey: akp.Public,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
sans := []x509util.SubjectAlternativeName{}
|
||||
uris := []*url.URL{{Scheme: "urn", Opaque: "ek:sha256:" + base64.StdEncoding.EncodeToString(keyID)}}
|
||||
asn1Value := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMModel, "SLB 9670 TPM2.0", oidTPMVersion, "7.55"))
|
||||
sans = append(sans, x509util.SubjectAlternativeName{
|
||||
Type: x509util.DirectoryNameType,
|
||||
ASN1Value: asn1Value,
|
||||
})
|
||||
ext, err := createSubjectAltNameExtension(nil, nil, nil, uris, sans, true)
|
||||
require.NoError(t, err)
|
||||
ext.Set(template)
|
||||
akCert, err := aca.Sign(template)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, akCert)
|
||||
|
||||
invalidTemplate := &x509.Certificate{
|
||||
PublicKey: akp.Public,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
invalidAKCert, err := aca.Sign(invalidTemplate)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, invalidAKCert)
|
||||
|
||||
// generate a JWK and the key authorization value
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
require.NoError(t, err)
|
||||
keyAuthorization, err := KeyAuthorization("token", jwk)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a new key attested by the AK, while including
|
||||
// the key authorization bytes as qualifying data.
|
||||
keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
|
||||
config := tpm.AttestKeyConfig{
|
||||
Algorithm: "RSA",
|
||||
Size: 2048,
|
||||
QualifyingData: keyAuthSum[:],
|
||||
}
|
||||
key, err := stpm.AttestKey(context.Background(), "first-ak", "first-key", config)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
params, err := key.CertificationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
signer, err := key.Signer(context.Background())
|
||||
require.NoError(t, err)
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
require.NoError(t, err)
|
||||
|
||||
// attest another key and get its certification parameters
|
||||
anotherKey, err := stpm.AttestKey(context.Background(), "first-ak", "another-key", config)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
anotherKeyParams, err := anotherKey.CertificationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
prov Provisioner
|
||||
ch *Challenge
|
||||
jwk *jose.JSONWebKey
|
||||
att *attestationObject
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *tpmAttestationData
|
||||
expErr *Error
|
||||
}{
|
||||
{"ok", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, nil},
|
||||
{"fail ver not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("ver not present")},
|
||||
{"fail ver type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": []interface{}{},
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("ver not present")},
|
||||
{"fail bogus ver", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "bogus",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError(`version "bogus" is not supported`)},
|
||||
{"fail x5c not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c not present")},
|
||||
{"fail x5c type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": [][]byte{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c not present")},
|
||||
{"fail x5c empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is empty")},
|
||||
{"fail leaf type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{"leaf", aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed")},
|
||||
{"fail leaf parse", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw[:100], aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed: x509: malformed certificate")},
|
||||
{"fail intermediate type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, "intermediate"},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed")},
|
||||
{"fail intermediate parse", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw[:100]},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed: x509: malformed certificate")},
|
||||
{"fail roots", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newInternalServerError("no root CA bundle available to verify the attestation certificate")},
|
||||
{"fail verify", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is not valid: x509: certificate signed by unknown authority")},
|
||||
{"fail validateAKCertificate", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{invalidAKCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("AK certificate is not valid: missing TPM manufacturer")},
|
||||
{"fail pubArea not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid pubArea in attestation statement")},
|
||||
{"fail pubArea type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": []interface{}{},
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid pubArea in attestation statement")},
|
||||
{"fail pubArea empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": []byte{},
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("pubArea is empty")},
|
||||
{"fail sig not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid sig in attestation statement")},
|
||||
{"fail sig type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": []interface{}{},
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid sig in attestation statement")},
|
||||
{"fail sig empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": []byte{},
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("sig is empty")},
|
||||
{"fail certInfo not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid certInfo in attestation statement")},
|
||||
{"fail certInfo type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": []interface{}{},
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid certInfo in attestation statement")},
|
||||
{"fail certInfo empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": []byte{},
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("certInfo is empty")},
|
||||
{"fail alg not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid alg in attestation statement")},
|
||||
{"fail alg type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(0), // invalid alg
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid alg 0 in attestation statement")},
|
||||
{"fail attestation verification", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": anotherKeyParams.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid certification parameters: certification refers to a different key")},
|
||||
{"fail keyAuthorization", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, &jose.JSONWebKey{Key: []byte("not an asymmetric key")}, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newInternalServerError("failed creating key auth digest: error generating JWK thumbprint: square/go-jose: unknown key type '[]uint8'")},
|
||||
{"fail different keyAuthorization", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "aDifferentToken"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), //
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("key authorization does not match")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := doTPMAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)
|
||||
if tt.expErr != nil {
|
||||
var ae *Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.EqualError(t, err, tt.expErr.Error())
|
||||
assert.Equal(t, ae.StatusCode(), tt.expErr.StatusCode())
|
||||
assert.Equal(t, ae.Type, tt.expErr.Type)
|
||||
}
|
||||
assert.Nil(t, got)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, got) {
|
||||
assert.Equal(t, akCert, got.Certificate)
|
||||
assert.Equal(t, [][]*x509.Certificate{
|
||||
{
|
||||
akCert, aca.Intermediate, aca.Root,
|
||||
},
|
||||
}, got.VerifiedChains)
|
||||
assert.Equal(t, fingerprint, got.Fingerprint)
|
||||
assert.Empty(t, got.PermanentIdentifiers) // currently expected to be always empty
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -56,7 +56,7 @@ func NewClient() Client {
|
|||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
// nolint:gosec // used on tls-alpn-01 challenge
|
||||
//nolint:gosec // used on tls-alpn-01 challenge
|
||||
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
|
||||
},
|
||||
},
|
||||
|
|
|
@ -29,10 +29,12 @@ type CertificateAuthority interface {
|
|||
}
|
||||
|
||||
// NewContext adds the given acme components to the context.
|
||||
func NewContext(ctx context.Context, db DB, client Client, linker Linker, fn PrerequisitesChecker) context.Context {
|
||||
func NewContext(ctx context.Context, db DB, client Client, linker Linker, fn PrerequisitesChecker,
|
||||
nnsServer string) context.Context {
|
||||
ctx = NewDatabaseContext(ctx, db)
|
||||
ctx = NewClientContext(ctx, client)
|
||||
ctx = NewLinkerContext(ctx, linker)
|
||||
ctx = NewNNSContext(ctx, nnsServer)
|
||||
// Prerequisite checker is optional.
|
||||
if fn != nil {
|
||||
ctx = NewPrerequisitesCheckerContext(ctx, fn)
|
||||
|
@ -46,7 +48,7 @@ type PrerequisitesChecker func(ctx context.Context) (bool, error)
|
|||
|
||||
// DefaultPrerequisitesChecker is the default PrerequisiteChecker and returns
|
||||
// always true.
|
||||
func DefaultPrerequisitesChecker(ctx context.Context) (bool, error) {
|
||||
func DefaultPrerequisitesChecker(context.Context) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,12 @@ import (
|
|||
// account.
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// IsErrNotFound returns true if the error is a "not found" error. Returns false
|
||||
// otherwise.
|
||||
func IsErrNotFound(err error) bool {
|
||||
return errors.Is(err, ErrNotFound)
|
||||
}
|
||||
|
||||
// DB is the DB interface expected by the step-ca ACME API.
|
||||
type DB interface {
|
||||
CreateAccount(ctx context.Context, acc *Account) error
|
||||
|
|
|
@ -13,12 +13,14 @@ import (
|
|||
|
||||
// dbAccount represents an ACME account.
|
||||
type dbAccount struct {
|
||||
ID string `json:"id"`
|
||||
Key *jose.JSONWebKey `json:"key"`
|
||||
Contact []string `json:"contact,omitempty"`
|
||||
Status acme.Status `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DeactivatedAt time.Time `json:"deactivatedAt"`
|
||||
ID string `json:"id"`
|
||||
Key *jose.JSONWebKey `json:"key"`
|
||||
Contact []string `json:"contact,omitempty"`
|
||||
Status acme.Status `json:"status"`
|
||||
LocationPrefix string `json:"locationPrefix"`
|
||||
ProvisionerName string `json:"provisionerName"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DeactivatedAt time.Time `json:"deactivatedAt"`
|
||||
}
|
||||
|
||||
func (dba *dbAccount) clone() *dbAccount {
|
||||
|
@ -26,7 +28,7 @@ func (dba *dbAccount) clone() *dbAccount {
|
|||
return &nu
|
||||
}
|
||||
|
||||
func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, error) {
|
||||
func (db *DB) getAccountIDByKeyID(_ context.Context, kid string) (string, error) {
|
||||
id, err := db.db.Get(accountByKeyIDTable, []byte(kid))
|
||||
if err != nil {
|
||||
if nosqlDB.IsErrNotFound(err) {
|
||||
|
@ -38,7 +40,7 @@ func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, erro
|
|||
}
|
||||
|
||||
// getDBAccount retrieves and unmarshals dbAccount.
|
||||
func (db *DB) getDBAccount(ctx context.Context, id string) (*dbAccount, error) {
|
||||
func (db *DB) getDBAccount(_ context.Context, id string) (*dbAccount, error) {
|
||||
data, err := db.db.Get(accountTable, []byte(id))
|
||||
if err != nil {
|
||||
if nosqlDB.IsErrNotFound(err) {
|
||||
|
@ -62,10 +64,12 @@ func (db *DB) GetAccount(ctx context.Context, id string) (*acme.Account, error)
|
|||
}
|
||||
|
||||
return &acme.Account{
|
||||
Status: dbacc.Status,
|
||||
Contact: dbacc.Contact,
|
||||
Key: dbacc.Key,
|
||||
ID: dbacc.ID,
|
||||
Status: dbacc.Status,
|
||||
Contact: dbacc.Contact,
|
||||
Key: dbacc.Key,
|
||||
ID: dbacc.ID,
|
||||
LocationPrefix: dbacc.LocationPrefix,
|
||||
ProvisionerName: dbacc.ProvisionerName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -87,11 +91,13 @@ func (db *DB) CreateAccount(ctx context.Context, acc *acme.Account) error {
|
|||
}
|
||||
|
||||
dba := &dbAccount{
|
||||
ID: acc.ID,
|
||||
Key: acc.Key,
|
||||
Contact: acc.Contact,
|
||||
Status: acc.Status,
|
||||
CreatedAt: clock.Now(),
|
||||
ID: acc.ID,
|
||||
Key: acc.Key,
|
||||
Contact: acc.Contact,
|
||||
Status: acc.Status,
|
||||
CreatedAt: clock.Now(),
|
||||
LocationPrefix: acc.LocationPrefix,
|
||||
ProvisionerName: acc.ProvisionerName,
|
||||
}
|
||||
|
||||
kid, err := acme.KeyToID(dba.Key)
|
||||
|
|
|
@ -95,16 +95,16 @@ func TestDB_getDBAccount(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if dbacc, err := d.getDBAccount(context.Background(), accID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -174,16 +174,16 @@ func TestDB_getAccountIDByKeyID(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if retAccID, err := d.getAccountIDByKeyID(context.Background(), kid); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -197,6 +197,8 @@ func TestDB_getAccountIDByKeyID(t *testing.T) {
|
|||
|
||||
func TestDB_GetAccount(t *testing.T) {
|
||||
accID := "accID"
|
||||
locationPrefix := "https://test.ca.smallstep.com/acme/foo/account/"
|
||||
provisionerName := "foo"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
|
@ -222,12 +224,14 @@ func TestDB_GetAccount(t *testing.T) {
|
|||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
dbacc := &dbAccount{
|
||||
ID: accID,
|
||||
Status: acme.StatusDeactivated,
|
||||
CreatedAt: now,
|
||||
DeactivatedAt: now,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
ID: accID,
|
||||
Status: acme.StatusDeactivated,
|
||||
CreatedAt: now,
|
||||
DeactivatedAt: now,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
LocationPrefix: locationPrefix,
|
||||
ProvisionerName: provisionerName,
|
||||
}
|
||||
b, err := json.Marshal(dbacc)
|
||||
assert.FatalError(t, err)
|
||||
|
@ -248,16 +252,16 @@ func TestDB_GetAccount(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if acc, err := d.GetAccount(context.Background(), accID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -266,6 +270,8 @@ func TestDB_GetAccount(t *testing.T) {
|
|||
assert.Equals(t, acc.ID, tc.dbacc.ID)
|
||||
assert.Equals(t, acc.Status, tc.dbacc.Status)
|
||||
assert.Equals(t, acc.Contact, tc.dbacc.Contact)
|
||||
assert.Equals(t, acc.LocationPrefix, tc.dbacc.LocationPrefix)
|
||||
assert.Equals(t, acc.ProvisionerName, tc.dbacc.ProvisionerName)
|
||||
assert.Equals(t, acc.Key.KeyID, tc.dbacc.Key.KeyID)
|
||||
}
|
||||
})
|
||||
|
@ -354,16 +360,16 @@ func TestDB_GetAccountByKeyID(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if acc, err := d.GetAccountByKeyID(context.Background(), kid); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -379,6 +385,7 @@ func TestDB_GetAccountByKeyID(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDB_CreateAccount(t *testing.T) {
|
||||
locationPrefix := "https://test.ca.smallstep.com/acme/foo/account/"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
acc *acme.Account
|
||||
|
@ -390,9 +397,10 @@ func TestDB_CreateAccount(t *testing.T) {
|
|||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
acc := &acme.Account{
|
||||
Status: acme.StatusValid,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
Status: acme.StatusValid,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
LocationPrefix: locationPrefix,
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
|
@ -413,9 +421,10 @@ func TestDB_CreateAccount(t *testing.T) {
|
|||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
acc := &acme.Account{
|
||||
Status: acme.StatusValid,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
Status: acme.StatusValid,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
LocationPrefix: locationPrefix,
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
|
@ -436,9 +445,10 @@ func TestDB_CreateAccount(t *testing.T) {
|
|||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
acc := &acme.Account{
|
||||
Status: acme.StatusValid,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
Status: acme.StatusValid,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
LocationPrefix: locationPrefix,
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
|
@ -456,6 +466,8 @@ func TestDB_CreateAccount(t *testing.T) {
|
|||
assert.FatalError(t, json.Unmarshal(nu, dbacc))
|
||||
assert.Equals(t, dbacc.ID, string(key))
|
||||
assert.Equals(t, dbacc.Contact, acc.Contact)
|
||||
assert.Equals(t, dbacc.LocationPrefix, acc.LocationPrefix)
|
||||
assert.Equals(t, dbacc.ProvisionerName, acc.ProvisionerName)
|
||||
assert.Equals(t, dbacc.Key.KeyID, acc.Key.KeyID)
|
||||
assert.True(t, clock.Now().Add(-time.Minute).Before(dbacc.CreatedAt))
|
||||
assert.True(t, clock.Now().Add(time.Minute).After(dbacc.CreatedAt))
|
||||
|
@ -479,9 +491,10 @@ func TestDB_CreateAccount(t *testing.T) {
|
|||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
acc := &acme.Account{
|
||||
Status: acme.StatusValid,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
Status: acme.StatusValid,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
LocationPrefix: locationPrefix,
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
|
@ -500,6 +513,8 @@ func TestDB_CreateAccount(t *testing.T) {
|
|||
assert.FatalError(t, json.Unmarshal(nu, dbacc))
|
||||
assert.Equals(t, dbacc.ID, string(key))
|
||||
assert.Equals(t, dbacc.Contact, acc.Contact)
|
||||
assert.Equals(t, dbacc.LocationPrefix, acc.LocationPrefix)
|
||||
assert.Equals(t, dbacc.ProvisionerName, acc.ProvisionerName)
|
||||
assert.Equals(t, dbacc.Key.KeyID, acc.Key.KeyID)
|
||||
assert.True(t, clock.Now().Add(-time.Minute).Before(dbacc.CreatedAt))
|
||||
assert.True(t, clock.Now().Add(time.Minute).After(dbacc.CreatedAt))
|
||||
|
@ -539,12 +554,14 @@ func TestDB_UpdateAccount(t *testing.T) {
|
|||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
dbacc := &dbAccount{
|
||||
ID: accID,
|
||||
Status: acme.StatusDeactivated,
|
||||
CreatedAt: now,
|
||||
DeactivatedAt: now,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
ID: accID,
|
||||
Status: acme.StatusDeactivated,
|
||||
CreatedAt: now,
|
||||
DeactivatedAt: now,
|
||||
Contact: []string{"foo", "bar"},
|
||||
LocationPrefix: "foo",
|
||||
ProvisionerName: "alpha",
|
||||
Key: jwk,
|
||||
}
|
||||
b, err := json.Marshal(dbacc)
|
||||
assert.FatalError(t, err)
|
||||
|
@ -644,10 +661,12 @@ func TestDB_UpdateAccount(t *testing.T) {
|
|||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
acc := &acme.Account{
|
||||
ID: accID,
|
||||
Status: acme.StatusDeactivated,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
ID: accID,
|
||||
Status: acme.StatusDeactivated,
|
||||
Contact: []string{"baz", "zap"},
|
||||
LocationPrefix: "bar",
|
||||
ProvisionerName: "beta",
|
||||
Key: jwk,
|
||||
}
|
||||
return test{
|
||||
acc: acc,
|
||||
|
@ -666,7 +685,10 @@ func TestDB_UpdateAccount(t *testing.T) {
|
|||
assert.FatalError(t, json.Unmarshal(nu, dbNew))
|
||||
assert.Equals(t, dbNew.ID, dbacc.ID)
|
||||
assert.Equals(t, dbNew.Status, acc.Status)
|
||||
assert.Equals(t, dbNew.Contact, dbacc.Contact)
|
||||
assert.Equals(t, dbNew.Contact, acc.Contact)
|
||||
// LocationPrefix should not change.
|
||||
assert.Equals(t, dbNew.LocationPrefix, dbacc.LocationPrefix)
|
||||
assert.Equals(t, dbNew.ProvisionerName, dbacc.ProvisionerName)
|
||||
assert.Equals(t, dbNew.Key.KeyID, dbacc.Key.KeyID)
|
||||
assert.Equals(t, dbNew.CreatedAt, dbacc.CreatedAt)
|
||||
assert.True(t, dbNew.DeactivatedAt.Add(-time.Minute).Before(now))
|
||||
|
@ -686,12 +708,7 @@ func TestDB_UpdateAccount(t *testing.T) {
|
|||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, tc.acc.ID, dbacc.ID)
|
||||
assert.Equals(t, tc.acc.Status, dbacc.Status)
|
||||
assert.Equals(t, tc.acc.Contact, dbacc.Contact)
|
||||
assert.Equals(t, tc.acc.Key.KeyID, dbacc.Key.KeyID)
|
||||
}
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ type dbAuthz struct {
|
|||
Identifier acme.Identifier `json:"identifier"`
|
||||
Status acme.Status `json:"status"`
|
||||
Token string `json:"token"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
ChallengeIDs []string `json:"challengeIDs"`
|
||||
Wildcard bool `json:"wildcard"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
|
@ -31,7 +32,7 @@ func (ba *dbAuthz) clone() *dbAuthz {
|
|||
|
||||
// getDBAuthz retrieves and unmarshals a database representation of the
|
||||
// ACME Authorization type.
|
||||
func (db *DB) getDBAuthz(ctx context.Context, id string) (*dbAuthz, error) {
|
||||
func (db *DB) getDBAuthz(_ context.Context, id string) (*dbAuthz, error) {
|
||||
data, err := db.db.Get(authzTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, acme.NewError(acme.ErrorMalformedType, "authz %s not found", id)
|
||||
|
@ -61,15 +62,16 @@ func (db *DB) GetAuthorization(ctx context.Context, id string) (*acme.Authorizat
|
|||
}
|
||||
}
|
||||
return &acme.Authorization{
|
||||
ID: dbaz.ID,
|
||||
AccountID: dbaz.AccountID,
|
||||
Identifier: dbaz.Identifier,
|
||||
Status: dbaz.Status,
|
||||
Challenges: chs,
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Token: dbaz.Token,
|
||||
Error: dbaz.Error,
|
||||
ID: dbaz.ID,
|
||||
AccountID: dbaz.AccountID,
|
||||
Identifier: dbaz.Identifier,
|
||||
Status: dbaz.Status,
|
||||
Challenges: chs,
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Token: dbaz.Token,
|
||||
Fingerprint: dbaz.Fingerprint,
|
||||
Error: dbaz.Error,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -97,6 +99,7 @@ func (db *DB) CreateAuthorization(ctx context.Context, az *acme.Authorization) e
|
|||
Identifier: az.Identifier,
|
||||
ChallengeIDs: chIDs,
|
||||
Token: az.Token,
|
||||
Fingerprint: az.Fingerprint,
|
||||
Wildcard: az.Wildcard,
|
||||
}
|
||||
|
||||
|
@ -111,14 +114,14 @@ func (db *DB) UpdateAuthorization(ctx context.Context, az *acme.Authorization) e
|
|||
}
|
||||
|
||||
nu := old.clone()
|
||||
|
||||
nu.Status = az.Status
|
||||
nu.Fingerprint = az.Fingerprint
|
||||
nu.Error = az.Error
|
||||
return db.save(ctx, old.ID, nu, old, "authz", authzTable)
|
||||
}
|
||||
|
||||
// GetAuthorizationsByAccountID retrieves and unmarshals ACME authz types from the database.
|
||||
func (db *DB) GetAuthorizationsByAccountID(ctx context.Context, accountID string) ([]*acme.Authorization, error) {
|
||||
func (db *DB) GetAuthorizationsByAccountID(_ context.Context, accountID string) ([]*acme.Authorization, error) {
|
||||
entries, err := db.db.List(authzTable)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error listing authz")
|
||||
|
@ -136,15 +139,16 @@ func (db *DB) GetAuthorizationsByAccountID(ctx context.Context, accountID string
|
|||
continue
|
||||
}
|
||||
authzs = append(authzs, &acme.Authorization{
|
||||
ID: dbaz.ID,
|
||||
AccountID: dbaz.AccountID,
|
||||
Identifier: dbaz.Identifier,
|
||||
Status: dbaz.Status,
|
||||
Challenges: nil, // challenges not required for current use case
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Token: dbaz.Token,
|
||||
Error: dbaz.Error,
|
||||
ID: dbaz.ID,
|
||||
AccountID: dbaz.AccountID,
|
||||
Identifier: dbaz.Identifier,
|
||||
Status: dbaz.Status,
|
||||
Challenges: nil, // challenges not required for current use case
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Token: dbaz.Token,
|
||||
Fingerprint: dbaz.Fingerprint,
|
||||
Error: dbaz.Error,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -101,16 +101,16 @@ func TestDB_getDBAuthz(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if dbaz, err := d.getDBAuthz(context.Background(), azID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -295,16 +295,16 @@ func TestDB_GetAuthorization(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if az, err := d.GetAuthorization(context.Background(), azID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -473,6 +473,7 @@ func TestDB_UpdateAuthorization(t *testing.T) {
|
|||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
ChallengeIDs: []string{"foo", "bar"},
|
||||
Wildcard: true,
|
||||
Fingerprint: "fingerprint",
|
||||
}
|
||||
b, err := json.Marshal(dbaz)
|
||||
assert.FatalError(t, err)
|
||||
|
@ -549,10 +550,11 @@ func TestDB_UpdateAuthorization(t *testing.T) {
|
|||
{ID: "foo"},
|
||||
{ID: "bar"},
|
||||
},
|
||||
Token: dbaz.Token,
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Error: acme.NewError(acme.ErrorMalformedType, "malformed"),
|
||||
Token: dbaz.Token,
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Fingerprint: "fingerprint",
|
||||
Error: acme.NewError(acme.ErrorMalformedType, "malformed"),
|
||||
}
|
||||
return test{
|
||||
az: updAz,
|
||||
|
@ -582,6 +584,7 @@ func TestDB_UpdateAuthorization(t *testing.T) {
|
|||
assert.Equals(t, dbNew.Wildcard, dbaz.Wildcard)
|
||||
assert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt)
|
||||
assert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt)
|
||||
assert.Equals(t, dbNew.Fingerprint, dbaz.Fingerprint)
|
||||
assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error())
|
||||
return nu, true, nil
|
||||
},
|
||||
|
@ -745,16 +748,16 @@ func TestDB_GetAuthorizationsByAccountID(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if azs, err := d.GetAuthorizationsByAccountID(context.Background(), accountID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ func (db *DB) CreateCertificate(ctx context.Context, cert *acme.Certificate) err
|
|||
|
||||
// GetCertificate retrieves and unmarshals an ACME certificate type from the
|
||||
// datastore.
|
||||
func (db *DB) GetCertificate(ctx context.Context, id string) (*acme.Certificate, error) {
|
||||
func (db *DB) GetCertificate(_ context.Context, id string) (*acme.Certificate, error) {
|
||||
b, err := db.db.Get(certTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, acme.NewError(acme.ErrorMalformedType, "certificate %s not found", id)
|
||||
|
@ -138,5 +138,4 @@ func parseBundle(b []byte) ([]*x509.Certificate, error) {
|
|||
return nil, errors.New("error decoding PEM: unexpected data")
|
||||
}
|
||||
return bundle, nil
|
||||
|
||||
}
|
||||
|
|
|
@ -250,16 +250,16 @@ func TestDB_GetCertificate(t *testing.T) {
|
|||
d := DB{db: tc.db}
|
||||
cert, err := d.GetCertificate(context.Background(), certID)
|
||||
if err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -444,16 +444,16 @@ func TestDB_GetCertificateBySerial(t *testing.T) {
|
|||
d := DB{db: tc.db}
|
||||
cert, err := d.GetCertificateBySerial(context.Background(), serial)
|
||||
if err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -6,8 +6,10 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
|
||||
"github.com/smallstep/nosql"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
)
|
||||
|
||||
type dbChallenge struct {
|
||||
|
@ -19,7 +21,7 @@ type dbChallenge struct {
|
|||
Value string `json:"value"`
|
||||
ValidatedAt string `json:"validatedAt"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Error *acme.Error `json:"error"`
|
||||
Error *acme.Error `json:"error"` // TODO(hs): a bit dangerous; should become db-specific type
|
||||
}
|
||||
|
||||
func (dbc *dbChallenge) clone() *dbChallenge {
|
||||
|
@ -27,7 +29,7 @@ func (dbc *dbChallenge) clone() *dbChallenge {
|
|||
return &u
|
||||
}
|
||||
|
||||
func (db *DB) getDBChallenge(ctx context.Context, id string) (*dbChallenge, error) {
|
||||
func (db *DB) getDBChallenge(_ context.Context, id string) (*dbChallenge, error) {
|
||||
data, err := db.db.Get(challengeTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, acme.NewError(acme.ErrorMalformedType, "challenge %s not found", id)
|
||||
|
@ -67,6 +69,7 @@ func (db *DB) CreateChallenge(ctx context.Context, ch *acme.Challenge) error {
|
|||
// GetChallenge retrieves and unmarshals an ACME challenge type from the database.
|
||||
// Implements the acme.DB GetChallenge interface.
|
||||
func (db *DB) GetChallenge(ctx context.Context, id, authzID string) (*acme.Challenge, error) {
|
||||
_ = authzID // unused input
|
||||
dbch, err := db.getDBChallenge(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -94,16 +94,16 @@ func TestDB_getDBChallenge(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if ch, err := d.getDBChallenge(context.Background(), chID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -286,16 +286,16 @@ func TestDB_GetChallenge(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if ch, err := d.GetChallenge(context.Background(), chID, azID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ type dbExternalAccountKeyReference struct {
|
|||
}
|
||||
|
||||
// getDBExternalAccountKey retrieves and unmarshals dbExternalAccountKey.
|
||||
func (db *DB) getDBExternalAccountKey(ctx context.Context, id string) (*dbExternalAccountKey, error) {
|
||||
func (db *DB) getDBExternalAccountKey(_ context.Context, id string) (*dbExternalAccountKey, error) {
|
||||
data, err := db.db.Get(externalAccountKeyTable, []byte(id))
|
||||
if err != nil {
|
||||
if nosqlDB.IsErrNotFound(err) {
|
||||
|
@ -54,7 +54,6 @@ func (db *DB) getDBExternalAccountKey(ctx context.Context, id string) (*dbExtern
|
|||
|
||||
// CreateExternalAccountKey creates a new External Account Binding key with a name
|
||||
func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
|
||||
externalAccountKeyMutex.Lock()
|
||||
defer externalAccountKeyMutex.Unlock()
|
||||
|
||||
|
@ -161,6 +160,8 @@ func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID
|
|||
|
||||
// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner
|
||||
func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {
|
||||
_, _ = cursor, limit // unused input
|
||||
|
||||
externalAccountKeyMutex.RLock()
|
||||
defer externalAccountKeyMutex.RUnlock()
|
||||
|
||||
|
@ -210,6 +211,7 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerI
|
|||
defer externalAccountKeyMutex.RUnlock()
|
||||
|
||||
if reference == "" {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -227,7 +229,8 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerI
|
|||
return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID)
|
||||
}
|
||||
|
||||
func (db *DB) GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||
func (db *DB) GetExternalAccountKeyByAccountID(context.Context, string, string) (*acme.ExternalAccountKey, error) {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -371,7 +374,6 @@ func sliceIndex(slice []string, item string) int {
|
|||
// removeElement deletes the item if it exists in the
|
||||
// slice. It returns a new slice, keeping the old one intact.
|
||||
func removeElement(slice []string, item string) []string {
|
||||
|
||||
newSlice := make([]string, 0)
|
||||
index := sliceIndex(slice, item)
|
||||
if index < 0 {
|
||||
|
|
|
@ -93,16 +93,16 @@ func TestDB_getDBExternalAccountKey(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -210,16 +210,16 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -374,16 +374,16 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -580,16 +580,16 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
|
|||
cursor, limit := "", 0
|
||||
if eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), provID, cursor, limit); err != nil {
|
||||
assert.Equals(t, "", nextCursor)
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.Equals(t, tc.err.Error(), err.Error())
|
||||
}
|
||||
|
@ -672,7 +672,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
|
|||
return errors.New("force default")
|
||||
}
|
||||
},
|
||||
MCmpAndSwap: func(bucket, key, old, new []byte) ([]byte, bool, error) {
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
fmt.Println(string(bucket))
|
||||
switch string(bucket) {
|
||||
case string(externalAccountKeyIDsByReferenceTable):
|
||||
|
@ -882,16 +882,16 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.Equals(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ func (db *DB) CreateNonce(ctx context.Context) (acme.Nonce, error) {
|
|||
|
||||
// DeleteNonce verifies that the nonce is valid (by checking if it exists),
|
||||
// and if so, consumes the nonce resource by deleting it from the database.
|
||||
func (db *DB) DeleteNonce(ctx context.Context, nonce acme.Nonce) error {
|
||||
func (db *DB) DeleteNonce(_ context.Context, nonce acme.Nonce) error {
|
||||
err := db.db.Update(&database.Tx{
|
||||
Operations: []*database.TxEntry{
|
||||
{
|
||||
|
|
|
@ -146,16 +146,16 @@ func TestDB_DeleteNonce(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if err := d.DeleteNonce(context.Background(), acme.Nonce(nonceID)); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ func New(db nosqlDB.DB) (*DB, error) {
|
|||
|
||||
// save writes the new data to the database, overwriting the old data if it
|
||||
// existed.
|
||||
func (db *DB) save(ctx context.Context, id string, nu, old interface{}, typ string, table []byte) error {
|
||||
func (db *DB) save(_ context.Context, id string, nu, old interface{}, typ string, table []byte) error {
|
||||
var (
|
||||
err error
|
||||
newB []byte
|
||||
|
|
|
@ -35,7 +35,7 @@ func (a *dbOrder) clone() *dbOrder {
|
|||
}
|
||||
|
||||
// getDBOrder retrieves and unmarshals an ACME Order type from the database.
|
||||
func (db *DB) getDBOrder(ctx context.Context, id string) (*dbOrder, error) {
|
||||
func (db *DB) getDBOrder(_ context.Context, id string) (*dbOrder, error) {
|
||||
b, err := db.db.Get(orderTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, acme.NewError(acme.ErrorMalformedType, "order %s not found", id)
|
||||
|
|
|
@ -102,16 +102,16 @@ func TestDB_getDBOrder(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if dbo, err := d.getDBOrder(context.Background(), orderID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -206,16 +206,16 @@ func TestDB_GetOrder(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if o, err := d.GetOrder(context.Background(), orderID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -1003,16 +1003,16 @@ func TestDB_updateAddOrderIDs(t *testing.T) {
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -65,6 +65,8 @@ const (
|
|||
ErrorUserActionRequiredType
|
||||
// ErrorNotImplementedType operation is not implemented
|
||||
ErrorNotImplementedType
|
||||
// ErrorNNSType was a problem with a NNS query during identifier validation
|
||||
ErrorNNSType
|
||||
)
|
||||
|
||||
// String returns the string representation of the acme problem type,
|
||||
|
@ -75,6 +77,8 @@ func (ap ProblemType) String() string {
|
|||
return "accountDoesNotExist"
|
||||
case ErrorAlreadyRevokedType:
|
||||
return "alreadyRevoked"
|
||||
case ErrorBadAttestationStatementType:
|
||||
return "badAttestationStatement"
|
||||
case ErrorBadCSRType:
|
||||
return "badCSR"
|
||||
case ErrorBadNonceType:
|
||||
|
@ -119,6 +123,8 @@ func (ap ProblemType) String() string {
|
|||
return "userActionRequired"
|
||||
case ErrorNotImplementedType:
|
||||
return "notImplemented"
|
||||
case ErrorNNSType:
|
||||
return "nns"
|
||||
default:
|
||||
return fmt.Sprintf("unsupported type ACME error type '%d'", int(ap))
|
||||
}
|
||||
|
@ -268,14 +274,34 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
// Error represents an ACME
|
||||
// Error represents an ACME Error
|
||||
type Error struct {
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
Subproblems []interface{} `json:"subproblems,omitempty"`
|
||||
Identifier interface{} `json:"identifier,omitempty"`
|
||||
Err error `json:"-"`
|
||||
Status int `json:"-"`
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
Subproblems []Subproblem `json:"subproblems,omitempty"`
|
||||
Err error `json:"-"`
|
||||
Status int `json:"-"`
|
||||
}
|
||||
|
||||
// Subproblem represents an ACME subproblem. It's fairly
|
||||
// similar to an ACME error, but differs in that it can't
|
||||
// include subproblems itself, the error is reflected
|
||||
// in the Detail property and doesn't have a Status.
|
||||
type Subproblem struct {
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
// The "identifier" field MUST NOT be present at the top level in ACME
|
||||
// problem documents. It can only be present in subproblems.
|
||||
// Subproblems need not all have the same type, and they do not need to
|
||||
// match the top level type.
|
||||
Identifier *Identifier `json:"identifier,omitempty"`
|
||||
}
|
||||
|
||||
// AddSubproblems adds the Subproblems to Error. It
|
||||
// returns the Error, allowing for fluent addition.
|
||||
func (e *Error) AddSubproblems(subproblems ...Subproblem) *Error {
|
||||
e.Subproblems = append(e.Subproblems, subproblems...)
|
||||
return e
|
||||
}
|
||||
|
||||
// NewError creates a new Error type.
|
||||
|
@ -283,6 +309,26 @@ func NewError(pt ProblemType, msg string, args ...interface{}) *Error {
|
|||
return newError(pt, errors.Errorf(msg, args...))
|
||||
}
|
||||
|
||||
// NewSubproblem creates a new Subproblem. The msg and args
|
||||
// are used to create a new error, which is set as the Detail, allowing
|
||||
// for more detailed error messages to be returned to the ACME client.
|
||||
func NewSubproblem(pt ProblemType, msg string, args ...interface{}) Subproblem {
|
||||
e := newError(pt, fmt.Errorf(msg, args...))
|
||||
s := Subproblem{
|
||||
Type: e.Type,
|
||||
Detail: e.Err.Error(),
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// NewSubproblemWithIdentifier creates a new Subproblem with a specific ACME
|
||||
// Identifier. It calls NewSubproblem and sets the Identifier.
|
||||
func NewSubproblemWithIdentifier(pt ProblemType, identifier Identifier, msg string, args ...interface{}) Subproblem {
|
||||
s := NewSubproblem(pt, msg, args...)
|
||||
s.Identifier = &identifier
|
||||
return s
|
||||
}
|
||||
|
||||
func newError(pt ProblemType, err error) *Error {
|
||||
meta, ok := errorMap[pt]
|
||||
if !ok {
|
||||
|
@ -310,10 +356,11 @@ func NewErrorISE(msg string, args ...interface{}) *Error {
|
|||
|
||||
// WrapError attempts to wrap the internal error.
|
||||
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
|
||||
switch e := err.(type) {
|
||||
case nil:
|
||||
var e *Error
|
||||
switch {
|
||||
case err == nil:
|
||||
return nil
|
||||
case *Error:
|
||||
case errors.As(err, &e):
|
||||
if e.Err == nil {
|
||||
e.Err = errors.Errorf(msg+"; "+e.Detail, args...)
|
||||
} else {
|
||||
|
|
122
acme/nns.go
Normal file
122
acme/nns.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
)
|
||||
|
||||
// multiSchemeClient unites invoker.RPCInvoke and common interface of
|
||||
// rpcclient.Client and rpcclient.WSClient.
|
||||
type multiSchemeClient interface {
|
||||
invoker.RPCInvoke
|
||||
// Init turns client to "ready-to-work" state.
|
||||
Init() error
|
||||
// Close closes connections.
|
||||
Close()
|
||||
// GetContractStateByID returns state of the NNS contract on 1 input.
|
||||
GetContractStateByID(int32) (*state.Contract, error)
|
||||
}
|
||||
|
||||
// NNS is used to interact with NNS contract.
|
||||
// Before work, the connection to the NNS server must be established using Dial method.
|
||||
type NNS struct {
|
||||
nnsContract util.Uint160
|
||||
client multiSchemeClient
|
||||
}
|
||||
|
||||
// NNSContext is used to store info about NNS server.
|
||||
type NNSContext struct {
|
||||
nnsServer string
|
||||
}
|
||||
|
||||
type nnsKey struct{}
|
||||
|
||||
// NewNNSContext adds new NNSContext with given params to the context.
|
||||
func NewNNSContext(ctx context.Context, nnsServer string) context.Context {
|
||||
return context.WithValue(ctx, nnsKey{}, NNSContext{nnsServer: nnsServer})
|
||||
}
|
||||
|
||||
// GetNNSContext returns NNSContext from the given context.
|
||||
func GetNNSContext(ctx context.Context) (NNSContext, bool) {
|
||||
c, ok := ctx.Value(nnsKey{}).(NNSContext)
|
||||
return c, ok
|
||||
}
|
||||
|
||||
// Dial connects to the address of the NNS server.
|
||||
// If URL address scheme is 'ws' or 'wss', then WebSocket protocol is used, otherwise HTTP.
|
||||
func (n *NNS) Dial(address string) error {
|
||||
var err error
|
||||
|
||||
uri, err := url.Parse(address)
|
||||
if err == nil && (uri.Scheme == "ws" || uri.Scheme == "wss") {
|
||||
n.client, err = rpcclient.NewWS(context.Background(), address, rpcclient.WSOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create Neo WebSocket client: %w", err)
|
||||
}
|
||||
} else {
|
||||
n.client, err = rpcclient.New(context.Background(), address, rpcclient.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create Neo HTTP client: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = n.client.Init(); err != nil {
|
||||
return fmt.Errorf("initialize Neo client: %w", err)
|
||||
}
|
||||
|
||||
nnsContract, err := n.client.GetContractStateByID(1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get NNS contract state: %w", err)
|
||||
}
|
||||
|
||||
n.nnsContract = nnsContract.Hash
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes connections of multiSchemeClient.
|
||||
func (n *NNS) Close() {
|
||||
n.client.Close()
|
||||
}
|
||||
|
||||
// GetTXTRecords returns TXT records of the provided domain by calling `getRecords` method of NNS contract.
|
||||
func (n *NNS) GetTXTRecords(name string) ([]string, error) {
|
||||
params, err := smartcontract.NewParametersFromValues(name, int64(nns.TXT))
|
||||
if err != nil {
|
||||
return make([]string, 0), fmt.Errorf("create slice of params: %w", err)
|
||||
}
|
||||
item, err := unwrap.Item(n.client.InvokeFunction(n.nnsContract, "getRecords", params, nil))
|
||||
if err != nil {
|
||||
return make([]string, 0), fmt.Errorf("contract invocation: %w", err)
|
||||
}
|
||||
|
||||
if _, ok := item.(stackitem.Null); !ok {
|
||||
arr, ok := item.Value().([]stackitem.Item)
|
||||
if !ok {
|
||||
return make([]string, 0), errors.New("invalid cast to stack item slice")
|
||||
}
|
||||
|
||||
var result = make([]string, 0, len(arr))
|
||||
for i := range arr {
|
||||
recordValue, err := arr[i].TryBytes()
|
||||
if err != nil {
|
||||
return make([]string, 0), fmt.Errorf("convert array item to byte slice: %w", err)
|
||||
}
|
||||
result = append(result, string(recordValue))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return make([]string, 0), errors.New("records not found")
|
||||
}
|
|
@ -3,6 +3,7 @@ package acme
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"net"
|
||||
|
@ -11,6 +12,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
|
@ -125,6 +127,27 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// getKeyFingerprint returns a fingerprint from the list of authorizations. This
|
||||
// fingerprint is used on the device-attest-01 flow to verify the attestation
|
||||
// certificate public key with the CSR public key.
|
||||
//
|
||||
// There's no point on reading all the authorizations as there will be only one
|
||||
// for a permanent identifier.
|
||||
func (o *Order) getAuthorizationFingerprint(ctx context.Context, db DB) (string, error) {
|
||||
for _, azID := range o.AuthorizationIDs {
|
||||
az, err := db.GetAuthorization(ctx, azID)
|
||||
if err != nil {
|
||||
return "", WrapErrorISE(err, "error getting authorization %q", azID)
|
||||
}
|
||||
// There's no point on reading all the authorizations as there will
|
||||
// be only one for a permanent identifier.
|
||||
if az.Fingerprint != "" {
|
||||
return az.Fingerprint, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Finalize signs a certificate if the necessary conditions for Order completion
|
||||
// have been met.
|
||||
//
|
||||
|
@ -150,6 +173,24 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
|
|||
return NewErrorISE("unexpected status %s for order %s", o.Status, o.ID)
|
||||
}
|
||||
|
||||
// Get key fingerprint if any. And then compare it with the CSR fingerprint.
|
||||
//
|
||||
// In device-attest-01 challenges we should check that the keys in the CSR
|
||||
// and the attestation certificate are the same.
|
||||
fingerprint, err := o.getAuthorizationFingerprint(ctx, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fingerprint != "" {
|
||||
fp, err := keyutil.Fingerprint(csr.PublicKey)
|
||||
if err != nil {
|
||||
return WrapErrorISE(err, "error calculating key fingerprint")
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(fingerprint), []byte(fp)) == 0 {
|
||||
return NewError(ErrorUnauthorizedType, "order %s csr does not match the attested key", o.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// canonicalize the CSR to allow for comparison
|
||||
csr = canonicalize(csr)
|
||||
|
||||
|
@ -165,6 +206,15 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
|
|||
for i := range o.Identifiers {
|
||||
if o.Identifiers[i].Type == PermanentIdentifier {
|
||||
permanentIdentifier = o.Identifiers[i].Value
|
||||
// the first (and only) Permanent Identifier that gets added to the certificate
|
||||
// should be equal to the Subject Common Name if it's set. If not equal, the CSR
|
||||
// is rejected, because the Common Name hasn't been challenged in that case. This
|
||||
// could result in unauthorized access if a relying system relies on the Common
|
||||
// Name in its authorization logic.
|
||||
if csr.Subject.CommonName != "" && csr.Subject.CommonName != permanentIdentifier {
|
||||
return NewError(ErrorBadCSRType, "CSR Subject Common Name does not match identifiers exactly: "+
|
||||
"CSR Subject Common Name = %s, Order Permanent Identifier = %s", csr.Subject.CommonName, permanentIdentifier)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -194,6 +244,14 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
|
|||
if err != nil {
|
||||
return WrapErrorISE(err, "error retrieving authorization options from ACME provisioner")
|
||||
}
|
||||
// Unlike most of the provisioners, ACME's AuthorizeSign method doesn't
|
||||
// define the templates, and the template data used in WebHooks is not
|
||||
// available.
|
||||
for _, signOp := range signOps {
|
||||
if wc, ok := signOp.(*provisioner.WebhookController); ok {
|
||||
wc.TemplateData = data
|
||||
}
|
||||
}
|
||||
|
||||
templateOptions, err := provisioner.CustomTemplateOptions(p.GetOptions(), data, defaultTemplate)
|
||||
if err != nil {
|
||||
|
@ -324,7 +382,6 @@ func numberOfIdentifierType(typ IdentifierType, ids []Identifier) int {
|
|||
// addresses or DNS names slice, depending on whether it can be parsed as an IP
|
||||
// or not. This might result in an additional SAN in the final certificate.
|
||||
func canonicalize(csr *x509.CertificateRequest) (canonicalized *x509.CertificateRequest) {
|
||||
|
||||
// for clarity only; we're operating on the same object by pointer
|
||||
canonicalized = csr
|
||||
|
||||
|
|
|
@ -2,9 +2,12 @@ package acme
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
|
@ -16,6 +19,7 @@ import (
|
|||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
|
@ -247,14 +251,14 @@ func TestOrder_UpdateStatus(t *testing.T) {
|
|||
tc := run(t)
|
||||
if err := tc.o.UpdateStatus(context.Background(), tc.db); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
@ -297,7 +301,7 @@ func (m *mockSignAuth) LoadProvisionerByName(name string) (provisioner.Interface
|
|||
return m.ret1.(provisioner.Interface), m.err
|
||||
}
|
||||
|
||||
func (m *mockSignAuth) IsRevoked(sn string) (bool, error) {
|
||||
func (m *mockSignAuth) IsRevoked(string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
@ -306,6 +310,14 @@ func (m *mockSignAuth) Revoke(context.Context, *authority.RevokeOptions) error {
|
|||
}
|
||||
|
||||
func TestOrder_Finalize(t *testing.T) {
|
||||
mustSigner := func(kty, crv string, size int) crypto.Signer {
|
||||
s, err := keyutil.GenerateSigner(kty, crv, size)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type test struct {
|
||||
o *Order
|
||||
err *Error
|
||||
|
@ -386,6 +398,72 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
err: NewErrorISE("unrecognized order status: %s", o.Status),
|
||||
}
|
||||
},
|
||||
"fail/non-matching-permanent-identifier-common-name": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
o := &Order{
|
||||
ID: "oID",
|
||||
AccountID: "accID",
|
||||
Status: StatusReady,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
AuthorizationIDs: []string{"a", "b"},
|
||||
Identifiers: []Identifier{
|
||||
{Type: "permanent-identifier", Value: "a-permanent-identifier"},
|
||||
},
|
||||
}
|
||||
|
||||
signer := mustSigner("EC", "P-256", 0)
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csr := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "a-different-identifier",
|
||||
},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
return test{
|
||||
o: o,
|
||||
csr: csr,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
switch id {
|
||||
case "a":
|
||||
return &Authorization{
|
||||
ID: id,
|
||||
Status: StatusValid,
|
||||
}, nil
|
||||
case "b":
|
||||
return &Authorization{
|
||||
ID: id,
|
||||
Fingerprint: fingerprint,
|
||||
Status: StatusValid,
|
||||
}, nil
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected authorization %s", id))
|
||||
return nil, errors.New("force")
|
||||
}
|
||||
},
|
||||
MockUpdateOrder: func(ctx context.Context, o *Order) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
err: &Error{
|
||||
Type: "urn:ietf:params:acme:error:badCSR",
|
||||
Detail: "The CSR is unacceptable",
|
||||
Status: 400,
|
||||
Err: fmt.Errorf("CSR Subject Common Name does not match identifiers exactly: "+
|
||||
"CSR Subject Common Name = %s, Order Permanent Identifier = %s", csr.Subject.CommonName, "a-permanent-identifier"),
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/error-provisioner-auth": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
o := &Order{
|
||||
|
@ -415,6 +493,11 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
},
|
||||
err: NewErrorISE("error retrieving authorization options from ACME provisioner: force"),
|
||||
}
|
||||
},
|
||||
|
@ -454,6 +537,11 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
}
|
||||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
},
|
||||
err: NewErrorISE("error creating template options from ACME provisioner: error unmarshaling template data: invalid character 'o' in literal false (expecting 'a')"),
|
||||
}
|
||||
},
|
||||
|
@ -495,6 +583,11 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
},
|
||||
err: NewErrorISE("error signing certificate for order oID: force"),
|
||||
}
|
||||
},
|
||||
|
@ -541,6 +634,9 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
assert.Equals(t, cert.OrderID, o.ID)
|
||||
|
@ -595,6 +691,9 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
|
@ -617,6 +716,297 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
err: NewErrorISE("error updating order oID: force"),
|
||||
}
|
||||
},
|
||||
"fail/csr-fingerprint": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
o := &Order{
|
||||
ID: "oID",
|
||||
AccountID: "accID",
|
||||
Status: StatusReady,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
AuthorizationIDs: []string{"a", "b"},
|
||||
Identifiers: []Identifier{
|
||||
{Type: "permanent-identifier", Value: "a-permanent-identifier"},
|
||||
},
|
||||
}
|
||||
|
||||
signer := mustSigner("EC", "P-256", 0)
|
||||
|
||||
csr := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "a-permanent-identifier",
|
||||
},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
leaf := &x509.Certificate{
|
||||
Subject: pkix.Name{CommonName: "a-permanent-identifier"},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
inter := &x509.Certificate{Subject: pkix.Name{CommonName: "inter"}}
|
||||
root := &x509.Certificate{Subject: pkix.Name{CommonName: "root"}}
|
||||
|
||||
return test{
|
||||
o: o,
|
||||
csr: csr,
|
||||
prov: &MockProvisioner{
|
||||
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
|
||||
assert.Equals(t, token, "")
|
||||
return nil, nil
|
||||
},
|
||||
MgetOptions: func() *provisioner.Options {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
ca: &mockSignAuth{
|
||||
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
assert.Equals(t, _csr, csr)
|
||||
return []*x509.Certificate{leaf, inter, root}, nil
|
||||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{
|
||||
ID: id,
|
||||
Fingerprint: "other-fingerprint",
|
||||
Status: StatusValid,
|
||||
}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
assert.Equals(t, cert.OrderID, o.ID)
|
||||
assert.Equals(t, cert.Leaf, leaf)
|
||||
assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})
|
||||
return nil
|
||||
},
|
||||
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
|
||||
assert.Equals(t, updo.CertificateID, "certID")
|
||||
assert.Equals(t, updo.Status, StatusValid)
|
||||
assert.Equals(t, updo.ID, o.ID)
|
||||
assert.Equals(t, updo.AccountID, o.AccountID)
|
||||
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
|
||||
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
|
||||
assert.Equals(t, updo.Identifiers, o.Identifiers)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
err: NewError(ErrorUnauthorizedType, "order oID csr does not match the attested key"),
|
||||
}
|
||||
},
|
||||
"ok/permanent-identifier": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
o := &Order{
|
||||
ID: "oID",
|
||||
AccountID: "accID",
|
||||
Status: StatusReady,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
AuthorizationIDs: []string{"a", "b"},
|
||||
Identifiers: []Identifier{
|
||||
{Type: "permanent-identifier", Value: "a-permanent-identifier"},
|
||||
},
|
||||
}
|
||||
|
||||
signer := mustSigner("EC", "P-256", 0)
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csr := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "a-permanent-identifier",
|
||||
},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
leaf := &x509.Certificate{
|
||||
Subject: pkix.Name{CommonName: "a-permanent-identifier"},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
inter := &x509.Certificate{Subject: pkix.Name{CommonName: "inter"}}
|
||||
root := &x509.Certificate{Subject: pkix.Name{CommonName: "root"}}
|
||||
|
||||
return test{
|
||||
o: o,
|
||||
csr: csr,
|
||||
prov: &MockProvisioner{
|
||||
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
|
||||
assert.Equals(t, token, "")
|
||||
return nil, nil
|
||||
},
|
||||
MgetOptions: func() *provisioner.Options {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
ca: &mockSignAuth{
|
||||
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
assert.Equals(t, _csr, csr)
|
||||
return []*x509.Certificate{leaf, inter, root}, nil
|
||||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
switch id {
|
||||
case "a":
|
||||
return &Authorization{
|
||||
ID: id,
|
||||
Status: StatusValid,
|
||||
}, nil
|
||||
case "b":
|
||||
return &Authorization{
|
||||
ID: id,
|
||||
Fingerprint: fingerprint,
|
||||
Status: StatusValid,
|
||||
}, nil
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected authorization %s", id))
|
||||
return nil, errors.New("force")
|
||||
}
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
assert.Equals(t, cert.OrderID, o.ID)
|
||||
assert.Equals(t, cert.Leaf, leaf)
|
||||
assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})
|
||||
return nil
|
||||
},
|
||||
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
|
||||
assert.Equals(t, updo.CertificateID, "certID")
|
||||
assert.Equals(t, updo.Status, StatusValid)
|
||||
assert.Equals(t, updo.ID, o.ID)
|
||||
assert.Equals(t, updo.AccountID, o.AccountID)
|
||||
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
|
||||
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
|
||||
assert.Equals(t, updo.Identifiers, o.Identifiers)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/permanent-identifier-only": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
o := &Order{
|
||||
ID: "oID",
|
||||
AccountID: "accID",
|
||||
Status: StatusReady,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
AuthorizationIDs: []string{"a", "b"},
|
||||
Identifiers: []Identifier{
|
||||
{Type: "dns", Value: "foo.internal"},
|
||||
{Type: "permanent-identifier", Value: "a-permanent-identifier"},
|
||||
},
|
||||
}
|
||||
|
||||
signer := mustSigner("EC", "P-256", 0)
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csr := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "a-permanent-identifier",
|
||||
},
|
||||
DNSNames: []string{"foo.internal"},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
leaf := &x509.Certificate{
|
||||
Subject: pkix.Name{CommonName: "a-permanent-identifier"},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
inter := &x509.Certificate{Subject: pkix.Name{CommonName: "inter"}}
|
||||
root := &x509.Certificate{Subject: pkix.Name{CommonName: "root"}}
|
||||
|
||||
return test{
|
||||
o: o,
|
||||
csr: csr,
|
||||
prov: &MockProvisioner{
|
||||
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
|
||||
assert.Equals(t, token, "")
|
||||
return nil, nil
|
||||
},
|
||||
MgetOptions: func() *provisioner.Options {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
// TODO(hs): we should work on making the mocks more realistic. Ideally, we should get rid of
|
||||
// the mock entirely, relying on an instances of provisioner, authority and DB (possibly hardest), so
|
||||
// that behavior of the tests is what an actual CA would do. We could gradually phase them out by
|
||||
// using the mocking functions as a wrapper for actual test helpers generated per test case or per
|
||||
// function that's tested.
|
||||
ca: &mockSignAuth{
|
||||
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
assert.Equals(t, _csr, csr)
|
||||
return []*x509.Certificate{leaf, inter, root}, nil
|
||||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{
|
||||
ID: id,
|
||||
Fingerprint: fingerprint,
|
||||
Status: StatusValid,
|
||||
}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
assert.Equals(t, cert.OrderID, o.ID)
|
||||
assert.Equals(t, cert.Leaf, leaf)
|
||||
assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})
|
||||
return nil
|
||||
},
|
||||
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
|
||||
assert.Equals(t, updo.CertificateID, "certID")
|
||||
assert.Equals(t, updo.Status, StatusValid)
|
||||
assert.Equals(t, updo.ID, o.ID)
|
||||
assert.Equals(t, updo.AccountID, o.AccountID)
|
||||
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
|
||||
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
|
||||
assert.Equals(t, updo.Identifiers, o.Identifiers)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/new-cert-dns": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
o := &Order{
|
||||
|
@ -660,6 +1050,9 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
|
@ -721,6 +1114,9 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
|
@ -785,6 +1181,9 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
|
@ -812,14 +1211,14 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
tc := run(t)
|
||||
if err := tc.o.Finalize(context.Background(), tc.db, tc.csr, tc.ca, tc.prov); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
@ -1474,14 +1873,14 @@ func TestOrder_sans(t *testing.T) {
|
|||
t.Errorf("Order.sans() = %v, want error; got none", got)
|
||||
return
|
||||
}
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tt.err.Type)
|
||||
assert.Equals(t, k.Detail, tt.err.Detail)
|
||||
assert.Equals(t, k.Status, tt.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tt.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tt.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
return
|
||||
|
@ -1492,3 +1891,55 @@ func TestOrder_sans(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrder_getAuthorizationFingerprint(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
type fields struct {
|
||||
AuthorizationIDs []string
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
db DB
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
}}, "", false},
|
||||
{"ok fingerprint", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
if id == "az1" {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
}
|
||||
return &Authorization{ID: id, Fingerprint: "fingerprint", Status: StatusValid}, nil
|
||||
},
|
||||
}}, "fingerprint", false},
|
||||
{"fail", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}}, "", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := &Order{
|
||||
AuthorizationIDs: tt.fields.AuthorizationIDs,
|
||||
}
|
||||
got, err := o.getAuthorizationFingerprint(tt.args.ctx, tt.args.db)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Order.getAuthorizationFingerprint() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("Order.getAuthorizationFingerprint() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
84
api/api.go
84
api/api.go
|
@ -1,9 +1,10 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/dsa" //nolint
|
||||
"crypto/dsa" //nolint:staticcheck // support legacy algorithms
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
|
@ -20,6 +21,8 @@ import (
|
|||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/smallstep/certificates/api/log"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
|
@ -40,6 +43,7 @@ type Authority interface {
|
|||
Root(shasum string) (*x509.Certificate, error)
|
||||
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
Renew(peer *x509.Certificate) ([]*x509.Certificate, error)
|
||||
RenewContext(ctx context.Context, peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||
Rekey(peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||
LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error)
|
||||
LoadProvisionerByName(string) (provisioner.Interface, error)
|
||||
|
@ -49,6 +53,7 @@ type Authority interface {
|
|||
GetRoots() ([]*x509.Certificate, error)
|
||||
GetFederation() ([]*x509.Certificate, error)
|
||||
Version() authority.Version
|
||||
GetCertificateRevocationList() ([]byte, error)
|
||||
}
|
||||
|
||||
// mustAuthority will be replaced on unit tests.
|
||||
|
@ -222,8 +227,39 @@ type RootResponse struct {
|
|||
// ProvisionersResponse is the response object that returns the list of
|
||||
// provisioners.
|
||||
type ProvisionersResponse struct {
|
||||
Provisioners provisioner.List `json:"provisioners"`
|
||||
NextCursor string `json:"nextCursor"`
|
||||
Provisioners provisioner.List
|
||||
NextCursor string
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler. It marshals the ProvisionersResponse
|
||||
// into a byte slice.
|
||||
//
|
||||
// Special treatment is given to the SCEP provisioner, as it contains a
|
||||
// challenge secret that MUST NOT be leaked in (public) HTTP responses. The
|
||||
// challenge value is thus redacted in HTTP responses.
|
||||
func (p ProvisionersResponse) MarshalJSON() ([]byte, error) {
|
||||
for _, item := range p.Provisioners {
|
||||
scepProv, ok := item.(*provisioner.SCEP)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
old := scepProv.ChallengePassword
|
||||
scepProv.ChallengePassword = "*** REDACTED ***"
|
||||
defer func(p string) { //nolint:gocritic // defer in loop required to restore initial state of provisioners
|
||||
scepProv.ChallengePassword = p
|
||||
}(old)
|
||||
}
|
||||
|
||||
var list = struct {
|
||||
Provisioners []provisioner.Interface `json:"provisioners"`
|
||||
NextCursor string `json:"nextCursor"`
|
||||
}{
|
||||
Provisioners: []provisioner.Interface(p.Provisioners),
|
||||
NextCursor: p.NextCursor,
|
||||
}
|
||||
|
||||
return json.Marshal(list)
|
||||
}
|
||||
|
||||
// ProvisionerKeyResponse is the response object that returns the encrypted key
|
||||
|
@ -255,7 +291,7 @@ func (h *caHandler) Route(r Router) {
|
|||
// New creates a new RouterHandler with the CA endpoints.
|
||||
//
|
||||
// Deprecated: Use api.Route(r Router)
|
||||
func New(auth Authority) RouterHandler {
|
||||
func New(Authority) RouterHandler {
|
||||
return &caHandler{}
|
||||
}
|
||||
|
||||
|
@ -267,6 +303,7 @@ func Route(r Router) {
|
|||
r.MethodFunc("POST", "/renew", Renew)
|
||||
r.MethodFunc("POST", "/rekey", Rekey)
|
||||
r.MethodFunc("POST", "/revoke", Revoke)
|
||||
r.MethodFunc("GET", "/crl", CRL)
|
||||
r.MethodFunc("GET", "/provisioners", Provisioners)
|
||||
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", ProvisionerKey)
|
||||
r.MethodFunc("GET", "/roots", Roots)
|
||||
|
@ -301,7 +338,7 @@ func Version(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// Health is an HTTP handler that returns the status of the server.
|
||||
func Health(w http.ResponseWriter, r *http.Request) {
|
||||
func Health(w http.ResponseWriter, _ *http.Request) {
|
||||
render.JSON(w, HealthResponse{Status: "ok"})
|
||||
}
|
||||
|
||||
|
@ -435,7 +472,7 @@ func logOtt(w http.ResponseWriter, token string) {
|
|||
}
|
||||
}
|
||||
|
||||
// LogCertificate add certificate fields to the log message.
|
||||
// LogCertificate adds certificate fields to the log message.
|
||||
func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) {
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
m := map[string]interface{}{
|
||||
|
@ -467,6 +504,41 @@ func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) {
|
|||
}
|
||||
}
|
||||
|
||||
// LogSSHCertificate adds SSH certificate fields to the log message.
|
||||
func LogSSHCertificate(w http.ResponseWriter, cert *ssh.Certificate) {
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
mak := bytes.TrimSpace(ssh.MarshalAuthorizedKey(cert))
|
||||
var certificate string
|
||||
parts := strings.Split(string(mak), " ")
|
||||
if len(parts) > 1 {
|
||||
certificate = parts[1]
|
||||
}
|
||||
var userOrHost string
|
||||
if cert.CertType == ssh.HostCert {
|
||||
userOrHost = "host"
|
||||
} else {
|
||||
userOrHost = "user"
|
||||
}
|
||||
certificateType := fmt.Sprintf("%s %s certificate", parts[0], userOrHost) // e.g. ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate
|
||||
m := map[string]interface{}{
|
||||
"serial": cert.Serial,
|
||||
"principals": cert.ValidPrincipals,
|
||||
"valid-from": time.Unix(int64(cert.ValidAfter), 0).Format(time.RFC3339),
|
||||
"valid-to": time.Unix(int64(cert.ValidBefore), 0).Format(time.RFC3339),
|
||||
"certificate": certificate,
|
||||
"certificate-type": certificateType,
|
||||
}
|
||||
fingerprint, err := sshutil.FormatFingerprint(mak, sshutil.DefaultFingerprint)
|
||||
if err == nil {
|
||||
fpParts := strings.Split(fingerprint, " ")
|
||||
if len(fpParts) > 3 {
|
||||
m["public-key"] = fmt.Sprintf("%s %s", fpParts[1], fpParts[len(fpParts)-1])
|
||||
}
|
||||
}
|
||||
rl.WithFields(m)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseCursor parses the cursor and limit from the request query params.
|
||||
func ParseCursor(r *http.Request) (cursor string, limit int, err error) {
|
||||
q := r.URL.Query()
|
||||
|
|
184
api/api_test.go
184
api/api_test.go
|
@ -4,7 +4,7 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/dsa" //nolint
|
||||
"crypto/dsa" //nolint:staticcheck // support legacy algorithms
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
|
@ -28,12 +28,15 @@ import (
|
|||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
sassert "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"golang.org/x/crypto/ssh"
|
||||
squarejose "gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/smallstep/assert"
|
||||
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
|
@ -192,6 +195,7 @@ type mockAuthority struct {
|
|||
sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
renew func(cert *x509.Certificate) ([]*x509.Certificate, error)
|
||||
rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||
renewContext func(ctx context.Context, oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||
loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error)
|
||||
loadProvisionerByName func(name string) (provisioner.Interface, error)
|
||||
getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error)
|
||||
|
@ -199,6 +203,7 @@ type mockAuthority struct {
|
|||
getEncryptedKey func(kid string) (string, error)
|
||||
getRoots func() ([]*x509.Certificate, error)
|
||||
getFederation func() ([]*x509.Certificate, error)
|
||||
getCRL func() ([]byte, error)
|
||||
signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
|
||||
signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||
renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||
|
@ -212,6 +217,14 @@ type mockAuthority struct {
|
|||
version func() authority.Version
|
||||
}
|
||||
|
||||
func (m *mockAuthority) GetCertificateRevocationList() ([]byte, error) {
|
||||
if m.getCRL != nil {
|
||||
return m.getCRL()
|
||||
}
|
||||
|
||||
return m.ret1.([]byte), m.err
|
||||
}
|
||||
|
||||
// TODO: remove once Authorize is deprecated.
|
||||
func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
||||
if m.authorize != nil {
|
||||
|
@ -255,6 +268,13 @@ func (m *mockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, erro
|
|||
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) RenewContext(ctx context.Context, oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
|
||||
if m.renewContext != nil {
|
||||
return m.renewContext(ctx, oldcert, pk)
|
||||
}
|
||||
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
||||
}
|
||||
|
||||
func (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
|
||||
if m.rekey != nil {
|
||||
return m.rekey(oldcert, pk)
|
||||
|
@ -772,6 +792,45 @@ func (m *mockProvisioner) AuthorizeSSHRekey(ctx context.Context, token string) (
|
|||
return m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err
|
||||
}
|
||||
|
||||
func Test_CRLGeneration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
statusCode int
|
||||
expected []byte
|
||||
}{
|
||||
{"empty", nil, http.StatusOK, nil},
|
||||
}
|
||||
|
||||
chiCtx := chi.NewRouteContext()
|
||||
req := httptest.NewRequest("GET", "http://example.com/crl", nil)
|
||||
req = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx))
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockMustAuthority(t, &mockAuthority{ret1: tt.expected, err: tt.err})
|
||||
w := httptest.NewRecorder()
|
||||
CRL(w, req)
|
||||
res := w.Result()
|
||||
|
||||
if res.StatusCode != tt.statusCode {
|
||||
t.Errorf("caHandler.CRL StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("caHandler.Root unexpected error = %v", err)
|
||||
}
|
||||
if tt.statusCode == 200 {
|
||||
if !bytes.Equal(bytes.TrimSpace(body), tt.expected) {
|
||||
t.Errorf("caHandler.Root CRL = %s, wants %s", body, tt.expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_caHandler_Route(t *testing.T) {
|
||||
type fields struct {
|
||||
Authority Authority
|
||||
|
@ -1508,3 +1567,122 @@ func mustCertificate(t *testing.T, pub, priv interface{}) *x509.Certificate {
|
|||
}
|
||||
return cert
|
||||
}
|
||||
|
||||
func TestProvisionersResponse_MarshalJSON(t *testing.T) {
|
||||
|
||||
k := map[string]any{
|
||||
"use": "sig",
|
||||
"kty": "EC",
|
||||
"kid": "4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc",
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"x": "7ZdAAMZCFU4XwgblI5RfZouBi8lYmF6DlZusNNnsbm8",
|
||||
"y": "sQr2JdzwD2fgyrymBEXWsxDxFNjjqN64qLLSbLdLZ9Y",
|
||||
}
|
||||
key := squarejose.JSONWebKey{}
|
||||
b, err := json.Marshal(k)
|
||||
assert.FatalError(t, err)
|
||||
err = json.Unmarshal(b, &key)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
r := ProvisionersResponse{
|
||||
Provisioners: provisioner.List{
|
||||
&provisioner.SCEP{
|
||||
Name: "scep",
|
||||
Type: "scep",
|
||||
ChallengePassword: "not-so-secret",
|
||||
MinimumPublicKeyLength: 2048,
|
||||
EncryptionAlgorithmIdentifier: 2,
|
||||
},
|
||||
&provisioner.JWK{
|
||||
EncryptedKey: "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg",
|
||||
Key: &key,
|
||||
Name: "step-cli",
|
||||
Type: "JWK",
|
||||
},
|
||||
},
|
||||
NextCursor: "next",
|
||||
}
|
||||
|
||||
expected := map[string]any{
|
||||
"provisioners": []map[string]any{
|
||||
{
|
||||
"type": "scep",
|
||||
"name": "scep",
|
||||
"challenge": "*** REDACTED ***",
|
||||
"minimumPublicKeyLength": 2048,
|
||||
"encryptionAlgorithmIdentifier": 2,
|
||||
},
|
||||
{
|
||||
"type": "JWK",
|
||||
"name": "step-cli",
|
||||
"key": map[string]any{
|
||||
"use": "sig",
|
||||
"kty": "EC",
|
||||
"kid": "4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc",
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"x": "7ZdAAMZCFU4XwgblI5RfZouBi8lYmF6DlZusNNnsbm8",
|
||||
"y": "sQr2JdzwD2fgyrymBEXWsxDxFNjjqN64qLLSbLdLZ9Y",
|
||||
},
|
||||
"encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg",
|
||||
},
|
||||
},
|
||||
"nextCursor": "next",
|
||||
}
|
||||
|
||||
expBytes, err := json.Marshal(expected)
|
||||
sassert.NoError(t, err)
|
||||
|
||||
br, err := r.MarshalJSON()
|
||||
sassert.NoError(t, err)
|
||||
sassert.JSONEq(t, string(expBytes), string(br))
|
||||
|
||||
keyCopy := key
|
||||
expList := provisioner.List{
|
||||
&provisioner.SCEP{
|
||||
Name: "scep",
|
||||
Type: "scep",
|
||||
ChallengePassword: "not-so-secret",
|
||||
MinimumPublicKeyLength: 2048,
|
||||
EncryptionAlgorithmIdentifier: 2,
|
||||
},
|
||||
&provisioner.JWK{
|
||||
EncryptedKey: "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg",
|
||||
Key: &keyCopy,
|
||||
Name: "step-cli",
|
||||
Type: "JWK",
|
||||
},
|
||||
}
|
||||
|
||||
// MarshalJSON must not affect the struct properties itself
|
||||
sassert.Equal(t, expList, r.Provisioners)
|
||||
}
|
||||
|
||||
const (
|
||||
fixtureECDSACertificate = `ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI herman`
|
||||
)
|
||||
|
||||
func TestLogSSHCertificate(t *testing.T) {
|
||||
|
||||
out, _, _, _, err := ssh.ParseAuthorizedKey([]byte(fixtureECDSACertificate))
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, ok := out.(*ssh.Certificate)
|
||||
require.True(t, ok)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
rl := logging.NewResponseLogger(w)
|
||||
LogSSHCertificate(rl, cert)
|
||||
|
||||
sassert.Equal(t, 200, w.Result().StatusCode)
|
||||
|
||||
fields := rl.Fields()
|
||||
sassert.Equal(t, uint64(14376510277651266987), fields["serial"])
|
||||
sassert.Equal(t, []string{"herman"}, fields["principals"])
|
||||
sassert.Equal(t, "ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate", fields["certificate-type"])
|
||||
sassert.Equal(t, time.Unix(1674129191, 0).Format(time.RFC3339), fields["valid-from"])
|
||||
sassert.Equal(t, time.Unix(1674186851, 0).Format(time.RFC3339), fields["valid-to"])
|
||||
sassert.Equal(t, "AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI", fields["certificate"])
|
||||
sassert.Equal(t, "SHA256:RvkDPGwl/G9d7LUFm1kmWhvOD9I/moPq4yxcb0STwr0 (ECDSA-CERT)", fields["public-key"])
|
||||
}
|
||||
|
|
32
api/crl.go
Normal file
32
api/crl.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"net/http"
|
||||
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
)
|
||||
|
||||
// CRL is an HTTP handler that returns the current CRL in DER or PEM format
|
||||
func CRL(w http.ResponseWriter, r *http.Request) {
|
||||
crlBytes, err := mustAuthority(r.Context()).GetCertificateRevocationList()
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, formatAsPEM := r.URL.Query()["pem"]
|
||||
if formatAsPEM {
|
||||
w.Header().Add("Content-Type", "application/x-pem-file")
|
||||
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.pem\"")
|
||||
|
||||
_ = pem.Encode(w, &pem.Block{
|
||||
Type: "X509 CRL",
|
||||
Bytes: crlBytes,
|
||||
})
|
||||
} else {
|
||||
w.Header().Add("Content-Type", "application/pkix-crl")
|
||||
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.der\"")
|
||||
w.Write(crlBytes)
|
||||
}
|
||||
}
|
|
@ -7,8 +7,6 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
// StackTracedError is the set of errors implementing the StackTrace function.
|
||||
|
@ -21,16 +19,21 @@ type StackTracedError interface {
|
|||
StackTrace() errors.StackTrace
|
||||
}
|
||||
|
||||
type fieldCarrier interface {
|
||||
WithFields(map[string]any)
|
||||
Fields() map[string]any
|
||||
}
|
||||
|
||||
// Error adds to the response writer the given error if it implements
|
||||
// logging.ResponseLogger. If it does not implement it, then writes the error
|
||||
// using the log package.
|
||||
func Error(rw http.ResponseWriter, err error) {
|
||||
rl, ok := rw.(logging.ResponseLogger)
|
||||
fc, ok := rw.(fieldCarrier)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
rl.WithFields(map[string]interface{}{
|
||||
fc.WithFields(map[string]any{
|
||||
"error": err,
|
||||
})
|
||||
|
||||
|
@ -38,23 +41,19 @@ func Error(rw http.ResponseWriter, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
e, ok := err.(StackTracedError)
|
||||
if !ok {
|
||||
e, ok = errors.Cause(err).(StackTracedError)
|
||||
}
|
||||
|
||||
if ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"stack-trace": fmt.Sprintf("%+v", e.StackTrace()),
|
||||
var st StackTracedError
|
||||
if errors.As(err, &st) {
|
||||
fc.WithFields(map[string]any{
|
||||
"stack-trace": fmt.Sprintf("%+v", st.StackTrace()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// EnabledResponse log the response object if it implements the EnableLogger
|
||||
// interface.
|
||||
func EnabledResponse(rw http.ResponseWriter, v interface{}) {
|
||||
func EnabledResponse(rw http.ResponseWriter, v any) {
|
||||
type enableLogger interface {
|
||||
ToLog() (interface{}, error)
|
||||
ToLog() (any, error)
|
||||
}
|
||||
|
||||
if el, ok := v.(enableLogger); ok {
|
||||
|
@ -65,8 +64,8 @@ func EnabledResponse(rw http.ResponseWriter, v interface{}) {
|
|||
return
|
||||
}
|
||||
|
||||
if rl, ok := rw.(logging.ResponseLogger); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
if rl, ok := rw.(fieldCarrier); ok {
|
||||
rl.WithFields(map[string]any{
|
||||
"response": out,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,43 +1,78 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
pkgerrors "github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
theError := errors.New("the error")
|
||||
type stackTracedError struct{}
|
||||
|
||||
type args struct {
|
||||
rw http.ResponseWriter
|
||||
err error
|
||||
func (stackTracedError) Error() string {
|
||||
return "a stacktraced error"
|
||||
}
|
||||
|
||||
func (stackTracedError) StackTrace() pkgerrors.StackTrace {
|
||||
f := struct{}{}
|
||||
return pkgerrors.StackTrace{ // fake stacktrace
|
||||
pkgerrors.Frame(unsafe.Pointer(&f)),
|
||||
pkgerrors.Frame(unsafe.Pointer(&f)),
|
||||
}
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
withFields bool
|
||||
name string
|
||||
error
|
||||
rw http.ResponseWriter
|
||||
isFieldCarrier bool
|
||||
stepDebug bool
|
||||
expectStackTrace bool
|
||||
}{
|
||||
{"normalLogger", args{httptest.NewRecorder(), theError}, false},
|
||||
{"responseLogger", args{logging.NewResponseLogger(httptest.NewRecorder()), theError}, true},
|
||||
{"noLogger", nil, nil, false, false, false},
|
||||
{"noError", nil, logging.NewResponseLogger(httptest.NewRecorder()), true, false, false},
|
||||
{"noErrorDebug", nil, logging.NewResponseLogger(httptest.NewRecorder()), true, true, false},
|
||||
{"anError", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), true, false, false},
|
||||
{"anErrorDebug", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), true, true, false},
|
||||
{"stackTracedError", new(stackTracedError), logging.NewResponseLogger(httptest.NewRecorder()), true, true, true},
|
||||
{"stackTracedErrorDebug", new(stackTracedError), logging.NewResponseLogger(httptest.NewRecorder()), true, true, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
Error(tt.args.rw, tt.args.err)
|
||||
if tt.withFields {
|
||||
if rl, ok := tt.args.rw.(logging.ResponseLogger); ok {
|
||||
fields := rl.Fields()
|
||||
if !reflect.DeepEqual(fields["error"], theError) {
|
||||
t.Errorf("ResponseLogger[\"error\"] = %s, wants %s", fields["error"], theError)
|
||||
}
|
||||
} else {
|
||||
t.Error("ResponseWriter does not implement logging.ResponseLogger")
|
||||
}
|
||||
if tt.stepDebug {
|
||||
t.Setenv("STEPDEBUG", "1")
|
||||
} else {
|
||||
t.Setenv("STEPDEBUG", "0")
|
||||
}
|
||||
|
||||
Error(tt.rw, tt.error)
|
||||
|
||||
// return early if test case doesn't use logger
|
||||
if !tt.isFieldCarrier {
|
||||
return
|
||||
}
|
||||
|
||||
fields := tt.rw.(logging.ResponseLogger).Fields()
|
||||
|
||||
// expect the error field to be (not) set and to be the same error that was fed to Error
|
||||
if tt.error == nil {
|
||||
assert.Nil(t, fields["error"])
|
||||
} else {
|
||||
assert.Same(t, tt.error, fields["error"])
|
||||
}
|
||||
|
||||
// check if stack-trace is set when expected
|
||||
if _, hasStackTrace := fields["stack-trace"]; tt.expectStackTrace && !hasStackTrace {
|
||||
t.Error(`ResponseLogger["stack-trace"] not set`)
|
||||
} else if !tt.expectStackTrace && hasStackTrace {
|
||||
t.Error(`ResponseLogger["stack-trace"] was set`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -41,8 +41,8 @@ func TestJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
if tt.wantErr {
|
||||
e, ok := err.(*errs.Error)
|
||||
if ok {
|
||||
var e *errs.Error
|
||||
if errors.As(err, &e) {
|
||||
if code := e.StatusCode(); code != 400 {
|
||||
t.Errorf("error.StatusCode() = %v, wants 400", code)
|
||||
}
|
||||
|
@ -102,14 +102,15 @@ func TestProtoJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
if tt.wantErr {
|
||||
switch err.(type) {
|
||||
case badProtoJSONError:
|
||||
var (
|
||||
ee *errs.Error
|
||||
bpe badProtoJSONError
|
||||
)
|
||||
switch {
|
||||
case errors.As(err, &bpe):
|
||||
assert.Contains(t, err.Error(), "syntax error")
|
||||
case *errs.Error:
|
||||
var ee *errs.Error
|
||||
if errors.As(err, &ee) {
|
||||
assert.Equal(t, http.StatusBadRequest, ee.Status)
|
||||
}
|
||||
case errors.As(err, &ee):
|
||||
assert.Equal(t, http.StatusBadRequest, ee.Status)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
@ -23,14 +23,25 @@ func JSON(w http.ResponseWriter, v interface{}) {
|
|||
// JSONStatus sets the Content-Type of w to application/json unless one is
|
||||
// specified.
|
||||
func JSONStatus(w http.ResponseWriter, v interface{}, status int) {
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(v); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
setContentTypeUnlessPresent(w, "application/json")
|
||||
w.WriteHeader(status)
|
||||
_, _ = b.WriteTo(w)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
var errUnsupportedType *json.UnsupportedTypeError
|
||||
if errors.As(err, &errUnsupportedType) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var errUnsupportedValue *json.UnsupportedValueError
|
||||
if errors.As(err, &errUnsupportedValue) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var errMarshalError *json.MarshalerError
|
||||
if errors.As(err, &errMarshalError) {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
log.EnabledResponse(w, v)
|
||||
}
|
||||
|
@ -77,8 +88,9 @@ type RenderableError interface {
|
|||
func Error(w http.ResponseWriter, err error) {
|
||||
log.Error(w, err)
|
||||
|
||||
if e, ok := err.(RenderableError); ok {
|
||||
e.Render(w)
|
||||
var r RenderableError
|
||||
if errors.As(err, &r) {
|
||||
r.Render(w)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -105,17 +117,18 @@ func statusCodeFromError(err error) (code int) {
|
|||
}
|
||||
|
||||
for err != nil {
|
||||
if sc, ok := err.(StatusCodedError); ok {
|
||||
var sc StatusCodedError
|
||||
if errors.As(err, &sc) {
|
||||
code = sc.StatusCode()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
cause, ok := err.(causer)
|
||||
if !ok {
|
||||
var c causer
|
||||
if !errors.As(err, &c) {
|
||||
break
|
||||
}
|
||||
err = cause.Cause()
|
||||
err = c.Cause()
|
||||
}
|
||||
|
||||
return
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
|
@ -26,10 +28,43 @@ func TestJSON(t *testing.T) {
|
|||
assert.Empty(t, rw.Fields())
|
||||
}
|
||||
|
||||
func TestJSONPanics(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
JSON(httptest.NewRecorder(), make(chan struct{}))
|
||||
})
|
||||
func TestJSONPanicsOnUnsupportedType(t *testing.T) {
|
||||
jsonPanicTest[json.UnsupportedTypeError](t, make(chan struct{}))
|
||||
}
|
||||
|
||||
func TestJSONPanicsOnUnsupportedValue(t *testing.T) {
|
||||
jsonPanicTest[json.UnsupportedValueError](t, math.NaN())
|
||||
}
|
||||
|
||||
func TestJSONPanicsOnMarshalerError(t *testing.T) {
|
||||
var v erroneousJSONMarshaler
|
||||
jsonPanicTest[json.MarshalerError](t, v)
|
||||
}
|
||||
|
||||
type erroneousJSONMarshaler struct{}
|
||||
|
||||
func (erroneousJSONMarshaler) MarshalJSON() ([]byte, error) {
|
||||
return nil, assert.AnError
|
||||
}
|
||||
|
||||
func jsonPanicTest[T json.UnsupportedTypeError | json.UnsupportedValueError | json.MarshalerError](t *testing.T, v any) {
|
||||
t.Helper()
|
||||
|
||||
defer func() {
|
||||
var err error
|
||||
if r := recover(); r == nil {
|
||||
t.Fatal("expected panic")
|
||||
} else if e, ok := r.(error); !ok {
|
||||
t.Fatalf("did not panic with an error (%T)", r)
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
|
||||
var e *T
|
||||
assert.ErrorAs(t, err, &e)
|
||||
}()
|
||||
|
||||
JSON(httptest.NewRecorder(), v)
|
||||
}
|
||||
|
||||
type renderableError struct {
|
||||
|
|
24
api/renew.go
24
api/renew.go
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
||||
|
@ -17,14 +18,22 @@ const (
|
|||
// Renew uses the information of certificate in the TLS connection to create a
|
||||
// new one.
|
||||
func Renew(w http.ResponseWriter, r *http.Request) {
|
||||
cert, err := getPeerCertificate(r)
|
||||
ctx := r.Context()
|
||||
|
||||
// Get the leaf certificate from the peer or the token.
|
||||
cert, token, err := getPeerCertificate(r)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
a := mustAuthority(r.Context())
|
||||
certChain, err := a.Renew(cert)
|
||||
// The token can be used by RAs to renew a certificate.
|
||||
if token != "" {
|
||||
ctx = authority.NewTokenContext(ctx, token)
|
||||
}
|
||||
|
||||
a := mustAuthority(ctx)
|
||||
certChain, err := a.RenewContext(ctx, cert, nil)
|
||||
if err != nil {
|
||||
render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew"))
|
||||
return
|
||||
|
@ -44,15 +53,16 @@ func Renew(w http.ResponseWriter, r *http.Request) {
|
|||
}, http.StatusCreated)
|
||||
}
|
||||
|
||||
func getPeerCertificate(r *http.Request) (*x509.Certificate, error) {
|
||||
func getPeerCertificate(r *http.Request) (*x509.Certificate, string, error) {
|
||||
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
|
||||
return r.TLS.PeerCertificates[0], nil
|
||||
return r.TLS.PeerCertificates[0], "", nil
|
||||
}
|
||||
if s := r.Header.Get(authorizationHeader); s != "" {
|
||||
if parts := strings.SplitN(s, bearerScheme+" ", 2); len(parts) == 2 {
|
||||
ctx := r.Context()
|
||||
return mustAuthority(ctx).AuthorizeRenewToken(ctx, parts[1])
|
||||
peer, err := mustAuthority(ctx).AuthorizeRenewToken(ctx, parts[1])
|
||||
return peer, parts[1], err
|
||||
}
|
||||
}
|
||||
return nil, errs.BadRequest("missing client certificate")
|
||||
return nil, "", errs.BadRequest("missing client certificate")
|
||||
}
|
||||
|
|
|
@ -62,12 +62,12 @@ func TestRevokeRequestValidate(t *testing.T) {
|
|||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.rr.Validate(); err != nil {
|
||||
switch v := err.(type) {
|
||||
case *errs.Error:
|
||||
assert.HasPrefix(t, v.Error(), tc.err.Error())
|
||||
assert.Equals(t, v.StatusCode(), tc.err.Status)
|
||||
default:
|
||||
t.Errorf("unexpected error type: %T", v)
|
||||
var ee *errs.Error
|
||||
if errors.As(err, &ee) {
|
||||
assert.HasPrefix(t, ee.Error(), tc.err.Error())
|
||||
assert.Equals(t, ee.StatusCode(), tc.err.Status)
|
||||
} else {
|
||||
t.Errorf("unexpected error type: %T", err)
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
|
|
|
@ -88,6 +88,7 @@ func Sign(w http.ResponseWriter, r *http.Request) {
|
|||
if len(certChainPEM) > 1 {
|
||||
caPEM = certChainPEM[1]
|
||||
}
|
||||
|
||||
LogCertificate(w, certChain[0])
|
||||
render.JSONStatus(w, &SignResponse{
|
||||
ServerPEM: certChainPEM[0],
|
||||
|
|
|
@ -338,6 +338,7 @@ func SSHSign(w http.ResponseWriter, r *http.Request) {
|
|||
identityCertificate = certChainToPEM(certChain)
|
||||
}
|
||||
|
||||
LogSSHCertificate(w, cert)
|
||||
render.JSONStatus(w, &SSHSignResponse{
|
||||
Certificate: SSHCertificate{cert},
|
||||
AddUserCertificate: addUserCertificate,
|
||||
|
|
|
@ -89,6 +89,7 @@ func SSHRekey(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
LogSSHCertificate(w, newCert)
|
||||
render.JSONStatus(w, &SSHRekeyResponse{
|
||||
Certificate: SSHCertificate{newCert},
|
||||
IdentityCertificate: identity,
|
||||
|
|
|
@ -81,6 +81,7 @@ func SSHRenew(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
LogSSHCertificate(w, newCert)
|
||||
render.JSONStatus(w, &SSHSignResponse{
|
||||
Certificate: SSHCertificate{newCert},
|
||||
IdentityCertificate: identity,
|
||||
|
|
|
@ -69,22 +69,21 @@ func NewACMEAdminResponder() ACMEAdminResponder {
|
|||
}
|
||||
|
||||
// GetExternalAccountKeys writes the response for the EAB keys GET endpoint
|
||||
func (h *acmeAdminResponder) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *acmeAdminResponder) GetExternalAccountKeys(w http.ResponseWriter, _ *http.Request) {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
|
||||
}
|
||||
|
||||
// CreateExternalAccountKey writes the response for the EAB key POST endpoint
|
||||
func (h *acmeAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *acmeAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, _ *http.Request) {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
|
||||
}
|
||||
|
||||
// DeleteExternalAccountKey writes the response for the EAB key DELETE endpoint
|
||||
func (h *acmeAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *acmeAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, _ *http.Request) {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
|
||||
}
|
||||
|
||||
func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey {
|
||||
|
||||
if k == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -229,11 +229,13 @@ func TestCreateAdminRequest_Validate(t *testing.T) {
|
|||
|
||||
if err != nil {
|
||||
assert.Type(t, &admin.Error{}, err)
|
||||
adminErr, _ := err.(*admin.Error)
|
||||
assert.Equals(t, tt.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tt.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, tt.err.Status, adminErr.Status)
|
||||
assert.Equals(t, tt.err.Message, adminErr.Message)
|
||||
var adminErr *admin.Error
|
||||
if assert.True(t, errors.As(err, &adminErr)) {
|
||||
assert.Equals(t, tt.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tt.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, tt.err.Status, adminErr.Status)
|
||||
assert.Equals(t, tt.err.Message, adminErr.Message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -278,11 +280,13 @@ func TestUpdateAdminRequest_Validate(t *testing.T) {
|
|||
|
||||
if err != nil {
|
||||
assert.Type(t, &admin.Error{}, err)
|
||||
adminErr, _ := err.(*admin.Error)
|
||||
assert.Equals(t, tt.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tt.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, tt.err.Status, adminErr.Status)
|
||||
assert.Equals(t, tt.err.Message, adminErr.Message)
|
||||
var ae *admin.Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.Equals(t, tt.err.Type, ae.Type)
|
||||
assert.Equals(t, tt.err.Detail, ae.Detail)
|
||||
assert.Equals(t, tt.err.Status, ae.Status)
|
||||
assert.Equals(t, tt.err.Message, ae.Message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,41 +4,47 @@ import (
|
|||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
)
|
||||
|
||||
// Handler is the Admin API request handler.
|
||||
type Handler struct {
|
||||
acmeResponder ACMEAdminResponder
|
||||
policyResponder PolicyAdminResponder
|
||||
}
|
||||
|
||||
// Route traffic and implement the Router interface.
|
||||
//
|
||||
// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder)
|
||||
func (h *Handler) Route(r api.Router) {
|
||||
Route(r, h.acmeResponder, h.policyResponder)
|
||||
}
|
||||
|
||||
// NewHandler returns a new Authority Config Handler.
|
||||
//
|
||||
// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder)
|
||||
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) api.RouterHandler {
|
||||
return &Handler{
|
||||
acmeResponder: acmeResponder,
|
||||
policyResponder: policyResponder,
|
||||
}
|
||||
}
|
||||
|
||||
var mustAuthority = func(ctx context.Context) adminAuthority {
|
||||
return authority.MustFromContext(ctx)
|
||||
}
|
||||
|
||||
type router struct {
|
||||
acmeResponder ACMEAdminResponder
|
||||
policyResponder PolicyAdminResponder
|
||||
webhookResponder WebhookAdminResponder
|
||||
}
|
||||
|
||||
type RouterOption func(*router)
|
||||
|
||||
func WithACMEResponder(acmeResponder ACMEAdminResponder) RouterOption {
|
||||
return func(r *router) {
|
||||
r.acmeResponder = acmeResponder
|
||||
}
|
||||
}
|
||||
|
||||
func WithPolicyResponder(policyResponder PolicyAdminResponder) RouterOption {
|
||||
return func(r *router) {
|
||||
r.policyResponder = policyResponder
|
||||
}
|
||||
}
|
||||
|
||||
func WithWebhookResponder(webhookResponder WebhookAdminResponder) RouterOption {
|
||||
return func(r *router) {
|
||||
r.webhookResponder = webhookResponder
|
||||
}
|
||||
}
|
||||
|
||||
// Route traffic and implement the Router interface.
|
||||
func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) {
|
||||
func Route(r api.Router, options ...RouterOption) {
|
||||
router := &router{}
|
||||
for _, fn := range options {
|
||||
fn(router)
|
||||
}
|
||||
|
||||
authnz := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return extractAuthorizeTokenAdmin(requireAPIEnabled(next))
|
||||
}
|
||||
|
@ -67,6 +73,10 @@ func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder Polic
|
|||
return authnz(disabledInStandalone(loadProvisionerByName(requireEABEnabled(loadExternalAccountKey(next)))))
|
||||
}
|
||||
|
||||
webhookMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return authnz(loadProvisionerByName(next))
|
||||
}
|
||||
|
||||
// Provisioners
|
||||
r.MethodFunc("GET", "/provisioners/{name}", authnz(GetProvisioner))
|
||||
r.MethodFunc("GET", "/provisioners", authnz(GetProvisioners))
|
||||
|
@ -82,36 +92,42 @@ func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder Polic
|
|||
r.MethodFunc("DELETE", "/admins/{id}", authnz(DeleteAdmin))
|
||||
|
||||
// ACME responder
|
||||
if acmeResponder != nil {
|
||||
if router.acmeResponder != nil {
|
||||
// ACME External Account Binding Keys
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.CreateExternalAccountKey))
|
||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(acmeResponder.DeleteExternalAccountKey))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(router.acmeResponder.CreateExternalAccountKey))
|
||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(router.acmeResponder.DeleteExternalAccountKey))
|
||||
}
|
||||
|
||||
// Policy responder
|
||||
if policyResponder != nil {
|
||||
if router.policyResponder != nil {
|
||||
// Policy - Authority
|
||||
r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(policyResponder.GetAuthorityPolicy))
|
||||
r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(policyResponder.CreateAuthorityPolicy))
|
||||
r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(policyResponder.UpdateAuthorityPolicy))
|
||||
r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(policyResponder.DeleteAuthorityPolicy))
|
||||
r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(router.policyResponder.GetAuthorityPolicy))
|
||||
r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(router.policyResponder.CreateAuthorityPolicy))
|
||||
r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(router.policyResponder.UpdateAuthorityPolicy))
|
||||
r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(router.policyResponder.DeleteAuthorityPolicy))
|
||||
|
||||
// Policy - Provisioner
|
||||
r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.GetProvisionerPolicy))
|
||||
r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.CreateProvisionerPolicy))
|
||||
r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.UpdateProvisionerPolicy))
|
||||
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.DeleteProvisionerPolicy))
|
||||
r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.GetProvisionerPolicy))
|
||||
r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.CreateProvisionerPolicy))
|
||||
r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.UpdateProvisionerPolicy))
|
||||
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.DeleteProvisionerPolicy))
|
||||
|
||||
// Policy - ACME Account
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy))
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.DeleteACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.DeleteACMEAccountPolicy))
|
||||
}
|
||||
|
||||
if router.webhookResponder != nil {
|
||||
r.MethodFunc("POST", "/provisioners/{provisionerName}/webhooks", webhookMiddleware(router.webhookResponder.CreateProvisionerWebhook))
|
||||
r.MethodFunc("PUT", "/provisioners/{provisionerName}/webhooks/{webhookName}", webhookMiddleware(router.webhookResponder.UpdateProvisionerWebhook))
|
||||
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/webhooks/{webhookName}", webhookMiddleware(router.webhookResponder.DeleteProvisionerWebhook))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ func requireAPIEnabled(next http.HandlerFunc) http.HandlerFunc {
|
|||
// extractAuthorizeTokenAdmin is a middleware that extracts and caches the bearer token.
|
||||
func extractAuthorizeTokenAdmin(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
tok := r.Header.Get("Authorization")
|
||||
if tok == "" {
|
||||
render.Error(w, admin.NewError(admin.ErrorUnauthorizedType,
|
||||
|
|
|
@ -50,7 +50,8 @@ func (par *policyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *ht
|
|||
|
||||
auth := mustAuthority(ctx)
|
||||
authorityPolicy, err := auth.GetAuthorityPolicy(r.Context())
|
||||
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
@ -74,7 +75,8 @@ func (par *policyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r
|
|||
auth := mustAuthority(ctx)
|
||||
authorityPolicy, err := auth.GetAuthorityPolicy(ctx)
|
||||
|
||||
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
@ -125,7 +127,8 @@ func (par *policyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r
|
|||
auth := mustAuthority(ctx)
|
||||
authorityPolicy, err := auth.GetAuthorityPolicy(ctx)
|
||||
|
||||
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
@ -175,7 +178,8 @@ func (par *policyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r
|
|||
auth := mustAuthority(ctx)
|
||||
authorityPolicy, err := auth.GetAuthorityPolicy(ctx)
|
||||
|
||||
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
@ -468,7 +472,6 @@ func isBadRequest(err error) bool {
|
|||
}
|
||||
|
||||
func validatePolicy(p *linkedca.Policy) error {
|
||||
|
||||
// convert the policy; return early if nil
|
||||
options := policy.LinkedToCertificates(p)
|
||||
if options == nil {
|
||||
|
|
235
authority/admin/api/webhook.go
Normal file
235
authority/admin/api/webhook.go
Normal file
|
@ -0,0 +1,235 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/api/read"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"go.step.sm/crypto/randutil"
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
// WebhookAdminResponder is the interface responsible for writing webhook admin
|
||||
// responses.
|
||||
type WebhookAdminResponder interface {
|
||||
CreateProvisionerWebhook(w http.ResponseWriter, r *http.Request)
|
||||
UpdateProvisionerWebhook(w http.ResponseWriter, r *http.Request)
|
||||
DeleteProvisionerWebhook(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// webhoookAdminResponder implements WebhookAdminResponder
|
||||
type webhookAdminResponder struct{}
|
||||
|
||||
// NewWebhookAdminResponder returns a new WebhookAdminResponder
|
||||
func NewWebhookAdminResponder() WebhookAdminResponder {
|
||||
return &webhookAdminResponder{}
|
||||
}
|
||||
|
||||
func validateWebhook(webhook *linkedca.Webhook) error {
|
||||
if webhook == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// name
|
||||
if webhook.Name == "" {
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook name is required")
|
||||
}
|
||||
|
||||
// url
|
||||
parsedURL, err := url.Parse(webhook.Url)
|
||||
if err != nil {
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
|
||||
}
|
||||
if parsedURL.Host == "" {
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
|
||||
}
|
||||
if parsedURL.Scheme != "https" {
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook url must use https")
|
||||
}
|
||||
if parsedURL.User != nil {
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook url may not contain username or password")
|
||||
}
|
||||
|
||||
// kind
|
||||
switch webhook.Kind {
|
||||
case linkedca.Webhook_ENRICHING, linkedca.Webhook_AUTHORIZING, linkedca.Webhook_SCEPCHALLENGE:
|
||||
default:
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook kind %q is invalid", webhook.Kind)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (war *webhookAdminResponder) CreateProvisionerWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
auth := mustAuthority(ctx)
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
|
||||
var newWebhook = new(linkedca.Webhook)
|
||||
if err := read.ProtoJSON(r.Body, newWebhook); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateWebhook(newWebhook); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
if newWebhook.Secret != "" {
|
||||
err := admin.NewError(admin.ErrorBadRequestType, "webhook secret must not be set")
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
if newWebhook.Id != "" {
|
||||
err := admin.NewError(admin.ErrorBadRequestType, "webhook ID must not be set")
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := randutil.UUIDv4()
|
||||
if err != nil {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error generating webhook id"))
|
||||
return
|
||||
}
|
||||
newWebhook.Id = id
|
||||
|
||||
// verify the name is unique
|
||||
for _, wh := range prov.Webhooks {
|
||||
if wh.Name == newWebhook.Name {
|
||||
err := admin.NewError(admin.ErrorConflictType, "provisioner %q already has a webhook with the name %q", prov.Name, newWebhook.Name)
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
secret, err := randutil.Bytes(64)
|
||||
if err != nil {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error generating webhook secret"))
|
||||
return
|
||||
}
|
||||
newWebhook.Secret = base64.StdEncoding.EncodeToString(secret)
|
||||
|
||||
prov.Webhooks = append(prov.Webhooks, newWebhook)
|
||||
|
||||
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
|
||||
if isBadRequest(err) {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner webhook"))
|
||||
return
|
||||
}
|
||||
|
||||
render.Error(w, admin.WrapErrorISE(err, "error creating provisioner webhook"))
|
||||
return
|
||||
}
|
||||
|
||||
render.ProtoJSONStatus(w, newWebhook, http.StatusCreated)
|
||||
}
|
||||
|
||||
func (war *webhookAdminResponder) DeleteProvisionerWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
auth := mustAuthority(ctx)
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
|
||||
webhookName := chi.URLParam(r, "webhookName")
|
||||
|
||||
found := false
|
||||
for i, wh := range prov.Webhooks {
|
||||
if wh.Name == webhookName {
|
||||
prov.Webhooks = append(prov.Webhooks[0:i], prov.Webhooks[i+1:]...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
|
||||
if isBadRequest(err) {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error deleting provisioner webhook"))
|
||||
return
|
||||
}
|
||||
|
||||
render.Error(w, admin.WrapErrorISE(err, "error deleting provisioner webhook"))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
|
||||
}
|
||||
|
||||
func (war *webhookAdminResponder) UpdateProvisionerWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
auth := mustAuthority(ctx)
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
|
||||
var newWebhook = new(linkedca.Webhook)
|
||||
if err := read.ProtoJSON(r.Body, newWebhook); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateWebhook(newWebhook); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
found := false
|
||||
for i, wh := range prov.Webhooks {
|
||||
if wh.Name != newWebhook.Name {
|
||||
continue
|
||||
}
|
||||
if newWebhook.Secret != "" && newWebhook.Secret != wh.Secret {
|
||||
err := admin.NewError(admin.ErrorBadRequestType, "webhook secret cannot be updated")
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
newWebhook.Secret = wh.Secret
|
||||
if newWebhook.Id != "" && newWebhook.Id != wh.Id {
|
||||
err := admin.NewError(admin.ErrorBadRequestType, "webhook ID cannot be updated")
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
newWebhook.Id = wh.Id
|
||||
prov.Webhooks[i] = newWebhook
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
msg := fmt.Sprintf("provisioner %q has no webhook with the name %q", prov.Name, newWebhook.Name)
|
||||
err := admin.NewError(admin.ErrorNotFoundType, msg)
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
|
||||
if isBadRequest(err) {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner webhook"))
|
||||
return
|
||||
}
|
||||
|
||||
render.Error(w, admin.WrapErrorISE(err, "error updating provisioner webhook"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return a copy without the signing secret. Include the client-supplied
|
||||
// auth secrets since those may have been updated in this request and we
|
||||
// should show in the response that they changed
|
||||
whResponse := &linkedca.Webhook{
|
||||
Id: newWebhook.Id,
|
||||
Name: newWebhook.Name,
|
||||
Url: newWebhook.Url,
|
||||
Kind: newWebhook.Kind,
|
||||
CertType: newWebhook.CertType,
|
||||
Auth: newWebhook.Auth,
|
||||
DisableTlsClientAuth: newWebhook.DisableTlsClientAuth,
|
||||
}
|
||||
render.ProtoJSONStatus(w, whResponse, http.StatusCreated)
|
||||
}
|
688
authority/admin/api/webhook_test.go
Normal file
688
authority/admin/api/webhook_test.go
Normal file
|
@ -0,0 +1,688 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.step.sm/linkedca"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
// ignore secret and id since those are set by the server
|
||||
func assertEqualWebhook(t *testing.T, a, b *linkedca.Webhook) {
|
||||
assert.Equal(t, a.Name, b.Name)
|
||||
assert.Equal(t, a.Url, b.Url)
|
||||
assert.Equal(t, a.Kind, b.Kind)
|
||||
assert.Equal(t, a.CertType, b.CertType)
|
||||
assert.Equal(t, a.DisableTlsClientAuth, b.DisableTlsClientAuth)
|
||||
|
||||
assert.Equal(t, a.GetAuth(), b.GetAuth())
|
||||
}
|
||||
|
||||
func TestWebhookAdminResponder_CreateProvisionerWebhook(t *testing.T) {
|
||||
type test struct {
|
||||
auth adminAuthority
|
||||
body []byte
|
||||
ctx context.Context
|
||||
err *admin.Error
|
||||
response *linkedca.Webhook
|
||||
statusCode int
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/existing-webhook": func(t *testing.T) test {
|
||||
webhook := &linkedca.Webhook{
|
||||
Name: "already-exists",
|
||||
Url: "https://example.com",
|
||||
}
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{webhook},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
err := admin.NewError(admin.ErrorConflictType, `provisioner "provName" already has a webhook with the name "already-exists"`)
|
||||
err.Message = `provisioner "provName" already has a webhook with the name "already-exists"`
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "already-exists",
|
||||
"url": "https://example.com",
|
||||
"kind": "ENRICHING"
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: err,
|
||||
statusCode: 409,
|
||||
}
|
||||
},
|
||||
"fail/read.ProtoJSON": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?")
|
||||
adminErr.Message = "proto: syntax error (line 1:2): invalid value ?"
|
||||
body := []byte("{?}")
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/missing-name": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook name is required")
|
||||
adminErr.Message = "webhook name is required"
|
||||
body := []byte(`{"url": "https://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/missing-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
|
||||
adminErr.Message = "webhook url is invalid"
|
||||
body := []byte(`{"name": "metadata", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/relative-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
|
||||
adminErr.Message = "webhook url is invalid"
|
||||
body := []byte(`{"name": "metadata", "url": "example.com/path", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/http-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url must use https")
|
||||
adminErr.Message = "webhook url must use https"
|
||||
body := []byte(`{"name": "metadata", "url": "http://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/basic-auth-in-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url may not contain username or password")
|
||||
adminErr.Message = "webhook url may not contain username or password"
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "metadata",
|
||||
"url": "https://user:pass@example.com",
|
||||
"kind": "ENRICHING"
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/secret-in-request": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook secret must not be set")
|
||||
adminErr.Message = "webhook secret must not be set"
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "metadata",
|
||||
"url": "https://example.com",
|
||||
"kind": "ENRICHING",
|
||||
"secret": "secret"
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/unsupported-webhook-kind": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, `(line 5:13): invalid value for enum type: "UNSUPPORTED"`)
|
||||
adminErr.Message = `(line 5:13): invalid value for enum type: "UNSUPPORTED"`
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "metadata",
|
||||
"url": "https://example.com",
|
||||
"kind": "UNSUPPORTED",
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/auth.UpdateProvisioner-error": func(t *testing.T) test {
|
||||
adm := &linkedca.Admin{
|
||||
Subject: "step",
|
||||
}
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithAdmin(context.Background(), adm)
|
||||
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
|
||||
adminErr := admin.NewError(admin.ErrorServerInternalType, "error creating provisioner webhook: force")
|
||||
adminErr.Message = "error creating provisioner webhook: force"
|
||||
body := []byte(`{"name": "metadata", "url": "https://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
auth: &mockAdminAuthority{
|
||||
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
|
||||
return &authority.PolicyError{
|
||||
Typ: authority.StoreFailure,
|
||||
Err: errors.New("force"),
|
||||
}
|
||||
},
|
||||
},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 500,
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
body := []byte(`{"name": "metadata", "url": "https://example.com", "kind": "ENRICHING", "certType": "X509"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
auth: &mockAdminAuthority{
|
||||
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
|
||||
assert.Equal(t, linkedca.Webhook_X509, nu.Webhooks[0].CertType)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
body: body,
|
||||
response: &linkedca.Webhook{
|
||||
Name: "metadata",
|
||||
Url: "https://example.com",
|
||||
Kind: linkedca.Webhook_ENRICHING,
|
||||
CertType: linkedca.Webhook_X509,
|
||||
},
|
||||
statusCode: 201,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mockMustAuthority(t, tc.auth)
|
||||
ctx := admin.NewContext(tc.ctx, &admin.MockDB{})
|
||||
war := NewWebhookAdminResponder()
|
||||
|
||||
req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body)))
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
war.CreateProvisionerWebhook(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equal(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
ae := testAdminError{}
|
||||
assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
|
||||
assert.Equal(t, tc.err.Type, ae.Type)
|
||||
assert.Equal(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equal(t, tc.err.Detail, ae.Detail)
|
||||
assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
// when the error message starts with "proto", we expect it to have
|
||||
// a syntax error (in the tests). If the message doesn't start with "proto",
|
||||
// we expect a full string match.
|
||||
if strings.HasPrefix(tc.err.Message, "proto:") {
|
||||
assert.True(t, strings.Contains(ae.Message, "syntax error"))
|
||||
} else {
|
||||
assert.Equal(t, tc.err.Message, ae.Message)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resp := &linkedca.Webhook{}
|
||||
body, err := io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, protojson.Unmarshal(body, resp))
|
||||
|
||||
assertEqualWebhook(t, tc.response, resp)
|
||||
assert.NotEmpty(t, resp.Secret)
|
||||
assert.NotEmpty(t, resp.Id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookAdminResponder_DeleteProvisionerWebhook(t *testing.T) {
|
||||
type test struct {
|
||||
auth adminAuthority
|
||||
err *admin.Error
|
||||
statusCode int
|
||||
provisionerWebhooks []*linkedca.Webhook
|
||||
webhookName string
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/auth.UpdateProvisioner-error": func(t *testing.T) test {
|
||||
adminErr := admin.NewError(admin.ErrorServerInternalType, "error deleting provisioner webhook: force")
|
||||
adminErr.Message = "error deleting provisioner webhook: force"
|
||||
return test{
|
||||
err: adminErr,
|
||||
auth: &mockAdminAuthority{
|
||||
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
|
||||
return &authority.PolicyError{
|
||||
Typ: authority.StoreFailure,
|
||||
Err: errors.New("force"),
|
||||
}
|
||||
},
|
||||
},
|
||||
statusCode: 500,
|
||||
webhookName: "my-webhook",
|
||||
provisionerWebhooks: []*linkedca.Webhook{
|
||||
{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING},
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/not-found": func(t *testing.T) test {
|
||||
return test{
|
||||
statusCode: 200,
|
||||
webhookName: "no-exists",
|
||||
provisionerWebhooks: nil,
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
return test{
|
||||
statusCode: 200,
|
||||
webhookName: "exists",
|
||||
auth: &mockAdminAuthority{
|
||||
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
|
||||
assert.Equal(t, nu.Webhooks, []*linkedca.Webhook{
|
||||
{Name: "my-2nd-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING},
|
||||
})
|
||||
return nil
|
||||
},
|
||||
},
|
||||
provisionerWebhooks: []*linkedca.Webhook{
|
||||
{Name: "exists", Url: "https.example.com", Kind: linkedca.Webhook_ENRICHING},
|
||||
{Name: "my-2nd-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mockMustAuthority(t, tc.auth)
|
||||
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("webhookName", tc.webhookName)
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: tc.provisionerWebhooks,
|
||||
}
|
||||
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
|
||||
ctx = admin.NewContext(ctx, &admin.MockDB{})
|
||||
req := httptest.NewRequest("DELETE", "/foo", nil).WithContext(ctx)
|
||||
|
||||
war := NewWebhookAdminResponder()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
war.DeleteProvisionerWebhook(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equal(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
ae := testAdminError{}
|
||||
assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
|
||||
assert.Equal(t, tc.err.Type, ae.Type)
|
||||
assert.Equal(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equal(t, tc.err.Detail, ae.Detail)
|
||||
assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
// when the error message starts with "proto", we expect it to have
|
||||
// a syntax error (in the tests). If the message doesn't start with "proto",
|
||||
// we expect a full string match.
|
||||
if strings.HasPrefix(tc.err.Message, "proto:") {
|
||||
assert.True(t, strings.Contains(ae.Message, "syntax error"))
|
||||
} else {
|
||||
assert.Equal(t, tc.err.Message, ae.Message)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
res.Body.Close()
|
||||
response := DeleteResponse{}
|
||||
assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &response))
|
||||
assert.Equal(t, "ok", response.Status)
|
||||
assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookAdminResponder_UpdateProvisionerWebhook(t *testing.T) {
|
||||
type test struct {
|
||||
auth adminAuthority
|
||||
adminDB admin.DB
|
||||
body []byte
|
||||
ctx context.Context
|
||||
err *admin.Error
|
||||
response *linkedca.Webhook
|
||||
statusCode int
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/not-found": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "exists", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
err := admin.NewError(admin.ErrorNotFoundType, `provisioner "provName" has no webhook with the name "no-exists"`)
|
||||
err.Message = `provisioner "provName" has no webhook with the name "no-exists"`
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "no-exists",
|
||||
"url": "https://example.com",
|
||||
"kind": "ENRICHING"
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: err,
|
||||
statusCode: 404,
|
||||
}
|
||||
},
|
||||
"fail/read.ProtoJSON": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?")
|
||||
adminErr.Message = "proto: syntax error (line 1:2): invalid value ?"
|
||||
body := []byte("{?}")
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/missing-name": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook name is required")
|
||||
adminErr.Message = "webhook name is required"
|
||||
body := []byte(`{"url": "https://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/missing-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
|
||||
adminErr.Message = "webhook url is invalid"
|
||||
body := []byte(`{"name": "metadata", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/relative-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
|
||||
adminErr.Message = "webhook url is invalid"
|
||||
body := []byte(`{"name": "metadata", "url": "example.com/path", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/http-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url must use https")
|
||||
adminErr.Message = "webhook url must use https"
|
||||
body := []byte(`{"name": "metadata", "url": "http://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/basic-auth-in-url": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url may not contain username or password")
|
||||
adminErr.Message = "webhook url may not contain username or password"
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "my-webhook",
|
||||
"url": "https://user:pass@example.com",
|
||||
"kind": "ENRICHING"
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/different-secret-in-request": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING, Secret: "c2VjcmV0"}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook secret cannot be updated")
|
||||
adminErr.Message = "webhook secret cannot be updated"
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "my-webhook",
|
||||
"url": "https://example.com",
|
||||
"kind": "ENRICHING",
|
||||
"secret": "secret"
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/auth.UpdateProvisioner-error": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorServerInternalType, "error updating provisioner webhook: force")
|
||||
adminErr.Message = "error updating provisioner webhook: force"
|
||||
body := []byte(`{"name": "my-webhook", "url": "https://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
auth: &mockAdminAuthority{
|
||||
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
|
||||
return &authority.PolicyError{
|
||||
Typ: authority.StoreFailure,
|
||||
Err: errors.New("force"),
|
||||
}
|
||||
},
|
||||
},
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 500,
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
body := []byte(`{"name": "my-webhook", "url": "https://example.com", "kind": "ENRICHING"}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
adminDB: &admin.MockDB{},
|
||||
auth: &mockAdminAuthority{
|
||||
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
body: body,
|
||||
response: &linkedca.Webhook{
|
||||
Name: "my-webhook",
|
||||
Url: "https://example.com",
|
||||
Kind: linkedca.Webhook_ENRICHING,
|
||||
},
|
||||
statusCode: 201,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mockMustAuthority(t, tc.auth)
|
||||
ctx := admin.NewContext(tc.ctx, tc.adminDB)
|
||||
war := NewWebhookAdminResponder()
|
||||
|
||||
req := httptest.NewRequest("PUT", "/foo", io.NopCloser(bytes.NewBuffer(tc.body)))
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
war.UpdateProvisionerWebhook(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equal(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
ae := testAdminError{}
|
||||
assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
|
||||
assert.Equal(t, tc.err.Type, ae.Type)
|
||||
assert.Equal(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equal(t, tc.err.Detail, ae.Detail)
|
||||
assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
// when the error message starts with "proto", we expect it to have
|
||||
// a syntax error (in the tests). If the message doesn't start with "proto",
|
||||
// we expect a full string match.
|
||||
if strings.HasPrefix(tc.err.Message, "proto:") {
|
||||
assert.True(t, strings.Contains(ae.Message, "syntax error"))
|
||||
} else {
|
||||
assert.Equal(t, tc.err.Message, ae.Message)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resp := &linkedca.Webhook{}
|
||||
body, err := io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, protojson.Unmarshal(body, resp))
|
||||
|
||||
assertEqualWebhook(t, tc.response, resp)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ func (dba *dbAdmin) clone() *dbAdmin {
|
|||
return &u
|
||||
}
|
||||
|
||||
func (db *DB) getDBAdminBytes(ctx context.Context, id string) ([]byte, error) {
|
||||
func (db *DB) getDBAdminBytes(_ context.Context, id string) ([]byte, error) {
|
||||
data, err := db.db.Get(adminsTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, admin.NewError(admin.ErrorNotFoundType, "admin %s not found", id)
|
||||
|
@ -102,7 +102,7 @@ func (db *DB) GetAdmin(ctx context.Context, id string) (*linkedca.Admin, error)
|
|||
// GetAdmins retrieves and unmarshals all active (not deleted) admins
|
||||
// from the database.
|
||||
// TODO should we be paginating?
|
||||
func (db *DB) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) {
|
||||
func (db *DB) GetAdmins(context.Context) ([]*linkedca.Admin, error) {
|
||||
dbEntries, err := db.db.List(adminsTable)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error loading admins")
|
||||
|
@ -111,16 +111,14 @@ func (db *DB) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) {
|
|||
for _, entry := range dbEntries {
|
||||
adm, err := db.unmarshalAdmin(entry.Value, string(entry.Key))
|
||||
if err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
if k.IsType(admin.ErrorDeletedType) || k.IsType(admin.ErrorAuthorityMismatchType) {
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if ae.IsType(admin.ErrorDeletedType) || ae.IsType(admin.ErrorAuthorityMismatchType) {
|
||||
continue
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if adm.AuthorityId != db.authorityID {
|
||||
continue
|
||||
|
|
|
@ -68,16 +68,16 @@ func TestDB_getDBAdminBytes(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if b, err := d.getDBAdminBytes(context.Background(), adminID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -192,16 +192,16 @@ func TestDB_getDBAdmin(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if dba, err := d.getDBAdmin(context.Background(), adminID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -280,16 +280,16 @@ func TestDB_unmarshalDBAdmin(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{authorityID: admin.DefaultAuthorityID}
|
||||
if dba, err := d.unmarshalDBAdmin(tc.in, adminID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -355,16 +355,16 @@ func TestDB_unmarshalAdmin(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{authorityID: admin.DefaultAuthorityID}
|
||||
if adm, err := d.unmarshalAdmin(tc.in, adminID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -509,16 +509,16 @@ func TestDB_GetAdmin(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if adm, err := d.GetAdmin(context.Background(), adminID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -661,16 +661,16 @@ func TestDB_DeleteAdmin(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if err := d.DeleteAdmin(context.Background(), adminID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -812,16 +812,16 @@ func TestDB_UpdateAdmin(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if err := d.UpdateAdmin(context.Background(), tc.adm); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -910,16 +910,16 @@ func TestDB_CreateAdmin(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if err := d.CreateAdmin(context.Background(), tc.adm); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -1086,16 +1086,16 @@ func TestDB_GetAdmins(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if admins, err := d.GetAdmins(context.Background()); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ func New(db nosqlDB.DB, authorityID string) (*DB, error) {
|
|||
|
||||
// save writes the new data to the database, overwriting the old data if it
|
||||
// existed.
|
||||
func (db *DB) save(ctx context.Context, id string, nu, old interface{}, typ string, table []byte) error {
|
||||
func (db *DB) save(_ context.Context, id string, nu, old interface{}, typ string, table []byte) error {
|
||||
var (
|
||||
err error
|
||||
newB []byte
|
||||
|
|
|
@ -71,7 +71,7 @@ func (dbap *dbAuthorityPolicy) convert() *linkedca.Policy {
|
|||
return dbToLinked(dbap.Policy)
|
||||
}
|
||||
|
||||
func (db *DB) getDBAuthorityPolicyBytes(ctx context.Context, authorityID string) ([]byte, error) {
|
||||
func (db *DB) getDBAuthorityPolicyBytes(_ context.Context, authorityID string) ([]byte, error) {
|
||||
data, err := db.db.Get(authorityPoliciesTable, []byte(authorityID))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, admin.NewError(admin.ErrorNotFoundType, "authority policy not found")
|
||||
|
@ -83,6 +83,7 @@ func (db *DB) getDBAuthorityPolicyBytes(ctx context.Context, authorityID string)
|
|||
|
||||
func (db *DB) unmarshalDBAuthorityPolicy(data []byte) (*dbAuthorityPolicy, error) {
|
||||
if len(data) == 0 {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
var dba = new(dbAuthorityPolicy)
|
||||
|
@ -102,6 +103,7 @@ func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*db
|
|||
return nil, err
|
||||
}
|
||||
if dbap == nil {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
if dbap.AuthorityID != authorityID {
|
||||
|
@ -112,7 +114,6 @@ func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*db
|
|||
}
|
||||
|
||||
func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||
|
||||
dbap := &dbAuthorityPolicy{
|
||||
ID: db.authorityID,
|
||||
AuthorityID: db.authorityID,
|
||||
|
@ -228,7 +229,6 @@ func dbToLinked(p *dbPolicy) *linkedca.Policy {
|
|||
}
|
||||
|
||||
func linkedToDB(p *linkedca.Policy) *dbPolicy {
|
||||
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -72,16 +72,16 @@ func TestDB_getDBAuthorityPolicyBytes(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if b, err := d.getDBAuthorityPolicyBytes(tc.ctx, tc.authorityID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -208,16 +208,16 @@ func TestDB_getDBAuthorityPolicy(t *testing.T) {
|
|||
dbp, err := d.getDBAuthorityPolicy(tc.ctx, tc.authorityID)
|
||||
switch {
|
||||
case err != nil:
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -309,16 +309,16 @@ func TestDB_CreateAuthorityPolicy(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: tc.authorityID}
|
||||
if err := d.CreateAuthorityPolicy(tc.ctx, tc.policy); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -406,16 +406,16 @@ func TestDB_GetAuthorityPolicy(t *testing.T) {
|
|||
d := DB{db: tc.db, authorityID: tc.authorityID}
|
||||
got, err := d.GetAuthorityPolicy(tc.ctx)
|
||||
if err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -578,16 +578,16 @@ func TestDB_UpdateAuthorityPolicy(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: tc.authorityID}
|
||||
if err := d.UpdateAuthorityPolicy(tc.ctx, tc.policy); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -718,16 +718,16 @@ func TestDB_DeleteAuthorityPolicy(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: tc.authorityID}
|
||||
if err := d.DeleteAuthorityPolicy(tc.ctx); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -24,6 +24,24 @@ type dbProvisioner struct {
|
|||
SSHTemplate *linkedca.Template `json:"sshTemplate"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DeletedAt time.Time `json:"deletedAt"`
|
||||
Webhooks []dbWebhook `json:"webhooks,omitempty"`
|
||||
}
|
||||
|
||||
type dbBasicAuth struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type dbWebhook struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
Kind string `json:"kind"`
|
||||
Secret string `json:"secret"`
|
||||
BearerToken string `json:"bearerToken,omitempty"`
|
||||
BasicAuth *dbBasicAuth `json:"basicAuth,omitempty"`
|
||||
DisableTLSClientAuth bool `json:"disableTLSClientAuth,omitempty"`
|
||||
CertType string `json:"certType,omitempty"`
|
||||
}
|
||||
|
||||
func (dbp *dbProvisioner) clone() *dbProvisioner {
|
||||
|
@ -48,10 +66,11 @@ func (dbp *dbProvisioner) convert2linkedca() (*linkedca.Provisioner, error) {
|
|||
SshTemplate: dbp.SSHTemplate,
|
||||
CreatedAt: timestamppb.New(dbp.CreatedAt),
|
||||
DeletedAt: timestamppb.New(dbp.DeletedAt),
|
||||
Webhooks: dbWebhooksToLinkedca(dbp.Webhooks),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (db *DB) getDBProvisionerBytes(ctx context.Context, id string) ([]byte, error) {
|
||||
func (db *DB) getDBProvisionerBytes(_ context.Context, id string) ([]byte, error) {
|
||||
data, err := db.db.Get(provisionersTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, admin.NewError(admin.ErrorNotFoundType, "provisioner %s not found", id)
|
||||
|
@ -113,7 +132,7 @@ func (db *DB) GetProvisioner(ctx context.Context, id string) (*linkedca.Provisio
|
|||
|
||||
// GetProvisioners retrieves and unmarshals all active (not deleted) provisioners
|
||||
// from the database.
|
||||
func (db *DB) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, error) {
|
||||
func (db *DB) GetProvisioners(_ context.Context) ([]*linkedca.Provisioner, error) {
|
||||
dbEntries, err := db.db.List(provisionersTable)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error loading provisioners")
|
||||
|
@ -122,16 +141,14 @@ func (db *DB) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, err
|
|||
for _, entry := range dbEntries {
|
||||
prov, err := db.unmarshalProvisioner(entry.Value, string(entry.Key))
|
||||
if err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
if k.IsType(admin.ErrorDeletedType) || k.IsType(admin.ErrorAuthorityMismatchType) {
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if ae.IsType(admin.ErrorDeletedType) || ae.IsType(admin.ErrorAuthorityMismatchType) {
|
||||
continue
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if prov.AuthorityId != db.authorityID {
|
||||
continue
|
||||
|
@ -164,6 +181,7 @@ func (db *DB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner)
|
|||
X509Template: prov.X509Template,
|
||||
SSHTemplate: prov.SshTemplate,
|
||||
CreatedAt: clock.Now(),
|
||||
Webhooks: linkedcaWebhooksToDB(prov.Webhooks),
|
||||
}
|
||||
|
||||
if err := db.save(ctx, prov.Id, dbp, nil, "provisioner", provisionersTable); err != nil {
|
||||
|
@ -193,6 +211,7 @@ func (db *DB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner)
|
|||
}
|
||||
nu.X509Template = prov.X509Template
|
||||
nu.SSHTemplate = prov.SshTemplate
|
||||
nu.Webhooks = linkedcaWebhooksToDB(prov.Webhooks)
|
||||
|
||||
return db.save(ctx, prov.Id, nu, old, "provisioner", provisionersTable)
|
||||
}
|
||||
|
@ -209,3 +228,70 @@ func (db *DB) DeleteProvisioner(ctx context.Context, id string) error {
|
|||
|
||||
return db.save(ctx, old.ID, nu, old, "provisioner", provisionersTable)
|
||||
}
|
||||
|
||||
func dbWebhooksToLinkedca(dbwhs []dbWebhook) []*linkedca.Webhook {
|
||||
if len(dbwhs) == 0 {
|
||||
return nil
|
||||
}
|
||||
lwhs := make([]*linkedca.Webhook, len(dbwhs))
|
||||
|
||||
for i, dbwh := range dbwhs {
|
||||
lwh := &linkedca.Webhook{
|
||||
Name: dbwh.Name,
|
||||
Id: dbwh.ID,
|
||||
Url: dbwh.URL,
|
||||
Kind: linkedca.Webhook_Kind(linkedca.Webhook_Kind_value[dbwh.Kind]),
|
||||
Secret: dbwh.Secret,
|
||||
DisableTlsClientAuth: dbwh.DisableTLSClientAuth,
|
||||
CertType: linkedca.Webhook_CertType(linkedca.Webhook_CertType_value[dbwh.CertType]),
|
||||
}
|
||||
if dbwh.BearerToken != "" {
|
||||
lwh.Auth = &linkedca.Webhook_BearerToken{
|
||||
BearerToken: &linkedca.BearerToken{
|
||||
BearerToken: dbwh.BearerToken,
|
||||
},
|
||||
}
|
||||
} else if dbwh.BasicAuth != nil && (dbwh.BasicAuth.Username != "" || dbwh.BasicAuth.Password != "") {
|
||||
lwh.Auth = &linkedca.Webhook_BasicAuth{
|
||||
BasicAuth: &linkedca.BasicAuth{
|
||||
Username: dbwh.BasicAuth.Username,
|
||||
Password: dbwh.BasicAuth.Password,
|
||||
},
|
||||
}
|
||||
}
|
||||
lwhs[i] = lwh
|
||||
}
|
||||
|
||||
return lwhs
|
||||
}
|
||||
|
||||
func linkedcaWebhooksToDB(lwhs []*linkedca.Webhook) []dbWebhook {
|
||||
if len(lwhs) == 0 {
|
||||
return nil
|
||||
}
|
||||
dbwhs := make([]dbWebhook, len(lwhs))
|
||||
|
||||
for i, lwh := range lwhs {
|
||||
dbwh := dbWebhook{
|
||||
Name: lwh.Name,
|
||||
ID: lwh.Id,
|
||||
URL: lwh.Url,
|
||||
Kind: lwh.Kind.String(),
|
||||
Secret: lwh.Secret,
|
||||
DisableTLSClientAuth: lwh.DisableTlsClientAuth,
|
||||
CertType: lwh.CertType.String(),
|
||||
}
|
||||
switch a := lwh.GetAuth().(type) {
|
||||
case *linkedca.Webhook_BearerToken:
|
||||
dbwh.BearerToken = a.BearerToken.BearerToken
|
||||
case *linkedca.Webhook_BasicAuth:
|
||||
dbwh.BasicAuth = &dbBasicAuth{
|
||||
Username: a.BasicAuth.Username,
|
||||
Password: a.BasicAuth.Password,
|
||||
}
|
||||
}
|
||||
dbwhs[i] = dbwh
|
||||
}
|
||||
|
||||
return dbwhs
|
||||
}
|
||||
|
|
|
@ -67,16 +67,16 @@ func TestDB_getDBProvisionerBytes(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if b, err := d.getDBProvisionerBytes(context.Background(), provID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -137,6 +137,7 @@ func TestDB_getDBProvisioner(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"fail/deleted": func(t *testing.T) test {
|
||||
|
||||
now := clock.Now()
|
||||
dbp := &dbProvisioner{
|
||||
ID: provID,
|
||||
|
@ -189,16 +190,16 @@ func TestDB_getDBProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if dbp, err := d.getDBProvisioner(context.Background(), provID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -210,6 +211,7 @@ func TestDB_getDBProvisioner(t *testing.T) {
|
|||
assert.Equals(t, dbp.Name, tc.dbp.Name)
|
||||
assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)
|
||||
assert.Fatal(t, dbp.DeletedAt.IsZero())
|
||||
assert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -275,16 +277,16 @@ func TestDB_unmarshalDBProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{authorityID: admin.DefaultAuthorityID}
|
||||
if dbp, err := d.unmarshalDBProvisioner(tc.in, provID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -300,6 +302,7 @@ func TestDB_unmarshalDBProvisioner(t *testing.T) {
|
|||
assert.Equals(t, dbp.SSHTemplate, tc.dbp.SSHTemplate)
|
||||
assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)
|
||||
assert.Fatal(t, dbp.DeletedAt.IsZero())
|
||||
assert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -353,6 +356,15 @@ func defaultDBP(t *testing.T) *dbProvisioner {
|
|||
Data: []byte("zap"),
|
||||
},
|
||||
CreatedAt: clock.Now(),
|
||||
Webhooks: []dbWebhook{
|
||||
{
|
||||
Name: "metadata",
|
||||
URL: "https://inventory.smallstep.com",
|
||||
Kind: linkedca.Webhook_ENRICHING.String(),
|
||||
Secret: "secret",
|
||||
BearerToken: "token",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -397,16 +409,16 @@ func TestDB_unmarshalProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{authorityID: admin.DefaultAuthorityID}
|
||||
if prov, err := d.unmarshalProvisioner(tc.in, provID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -419,6 +431,7 @@ func TestDB_unmarshalProvisioner(t *testing.T) {
|
|||
assert.Equals(t, prov.Claims, tc.dbp.Claims)
|
||||
assert.Equals(t, prov.X509Template, tc.dbp.X509Template)
|
||||
assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)
|
||||
assert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -535,16 +548,16 @@ func TestDB_GetProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if prov, err := d.GetProvisioner(context.Background(), provID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -557,6 +570,7 @@ func TestDB_GetProvisioner(t *testing.T) {
|
|||
assert.Equals(t, prov.Claims, tc.dbp.Claims)
|
||||
assert.Equals(t, prov.X509Template, tc.dbp.X509Template)
|
||||
assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)
|
||||
assert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -629,6 +643,7 @@ func TestDB_DeleteProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)
|
||||
assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)
|
||||
assert.Equals(t, _dbp.Details, dbp.Details)
|
||||
assert.Equals(t, _dbp.Webhooks, dbp.Webhooks)
|
||||
|
||||
assert.True(t, _dbp.DeletedAt.Before(time.Now()))
|
||||
assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute)))
|
||||
|
@ -668,6 +683,7 @@ func TestDB_DeleteProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)
|
||||
assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)
|
||||
assert.Equals(t, _dbp.Details, dbp.Details)
|
||||
assert.Equals(t, _dbp.Webhooks, dbp.Webhooks)
|
||||
|
||||
assert.True(t, _dbp.DeletedAt.Before(time.Now()))
|
||||
assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute)))
|
||||
|
@ -683,16 +699,16 @@ func TestDB_DeleteProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if err := d.DeleteProvisioner(context.Background(), provID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -819,6 +835,7 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
assert.Equals(t, provs[0].Claims, fooProv.Claims)
|
||||
assert.Equals(t, provs[0].X509Template, fooProv.X509Template)
|
||||
assert.Equals(t, provs[0].SshTemplate, fooProv.SSHTemplate)
|
||||
assert.Equals(t, provs[0].Webhooks, dbWebhooksToLinkedca(fooProv.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(provs[0].Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -831,6 +848,7 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
assert.Equals(t, provs[1].Claims, zapProv.Claims)
|
||||
assert.Equals(t, provs[1].X509Template, zapProv.X509Template)
|
||||
assert.Equals(t, provs[1].SshTemplate, zapProv.SSHTemplate)
|
||||
assert.Equals(t, provs[1].Webhooks, dbWebhooksToLinkedca(zapProv.Webhooks))
|
||||
|
||||
retDetailsBytes, err = json.Marshal(provs[1].Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -844,16 +862,16 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if provs, err := d.GetProvisioners(context.Background()); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -895,6 +913,7 @@ func TestDB_CreateProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -932,6 +951,7 @@ func TestDB_CreateProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -952,16 +972,16 @@ func TestDB_CreateProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if err := d.CreateProvisioner(context.Background(), tc.prov); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -1080,6 +1100,7 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -1141,6 +1162,12 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
prov.Webhooks = []*linkedca.Webhook{
|
||||
{
|
||||
Name: "users",
|
||||
Url: "https://example.com/users",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(dbp)
|
||||
assert.FatalError(t, err)
|
||||
|
@ -1168,6 +1195,7 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -1188,16 +1216,16 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if err := d.UpdateProvisioner(context.Background(), tc.prov); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -1206,3 +1234,164 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_linkedcaWebhooksToDB(t *testing.T) {
|
||||
type test struct {
|
||||
in []*linkedca.Webhook
|
||||
want []dbWebhook
|
||||
}
|
||||
var tests = map[string]test{
|
||||
"nil": {
|
||||
in: nil,
|
||||
want: nil,
|
||||
},
|
||||
"zero": {
|
||||
in: []*linkedca.Webhook{},
|
||||
want: nil,
|
||||
},
|
||||
"bearer": {
|
||||
in: []*linkedca.Webhook{
|
||||
{
|
||||
Name: "bearer",
|
||||
Url: "https://example.com",
|
||||
Kind: linkedca.Webhook_ENRICHING,
|
||||
Secret: "secret",
|
||||
Auth: &linkedca.Webhook_BearerToken{
|
||||
BearerToken: &linkedca.BearerToken{
|
||||
BearerToken: "token",
|
||||
},
|
||||
},
|
||||
DisableTlsClientAuth: true,
|
||||
CertType: linkedca.Webhook_X509,
|
||||
},
|
||||
},
|
||||
want: []dbWebhook{
|
||||
{
|
||||
Name: "bearer",
|
||||
URL: "https://example.com",
|
||||
Kind: "ENRICHING",
|
||||
Secret: "secret",
|
||||
BearerToken: "token",
|
||||
DisableTLSClientAuth: true,
|
||||
CertType: linkedca.Webhook_X509.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
"basic": {
|
||||
in: []*linkedca.Webhook{
|
||||
{
|
||||
Name: "basic",
|
||||
Url: "https://example.com",
|
||||
Kind: linkedca.Webhook_ENRICHING,
|
||||
Secret: "secret",
|
||||
Auth: &linkedca.Webhook_BasicAuth{
|
||||
BasicAuth: &linkedca.BasicAuth{
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []dbWebhook{
|
||||
{
|
||||
Name: "basic",
|
||||
URL: "https://example.com",
|
||||
Kind: "ENRICHING",
|
||||
Secret: "secret",
|
||||
BasicAuth: &dbBasicAuth{
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
CertType: linkedca.Webhook_ALL.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := linkedcaWebhooksToDB(tc.in)
|
||||
assert.Equals(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_dbWebhooksToLinkedca(t *testing.T) {
|
||||
type test struct {
|
||||
in []dbWebhook
|
||||
want []*linkedca.Webhook
|
||||
}
|
||||
var tests = map[string]test{
|
||||
"nil": {
|
||||
in: nil,
|
||||
want: nil,
|
||||
},
|
||||
"zero": {
|
||||
in: []dbWebhook{},
|
||||
want: nil,
|
||||
},
|
||||
"bearer": {
|
||||
in: []dbWebhook{
|
||||
{
|
||||
Name: "bearer",
|
||||
ID: "69350cb6-6c31-4b5e-bf25-affd5053427d",
|
||||
URL: "https://example.com",
|
||||
Kind: "ENRICHING",
|
||||
Secret: "secret",
|
||||
BearerToken: "token",
|
||||
DisableTLSClientAuth: true,
|
||||
},
|
||||
},
|
||||
want: []*linkedca.Webhook{
|
||||
{
|
||||
Name: "bearer",
|
||||
Id: "69350cb6-6c31-4b5e-bf25-affd5053427d",
|
||||
Url: "https://example.com",
|
||||
Kind: linkedca.Webhook_ENRICHING,
|
||||
Secret: "secret",
|
||||
Auth: &linkedca.Webhook_BearerToken{
|
||||
BearerToken: &linkedca.BearerToken{
|
||||
BearerToken: "token",
|
||||
},
|
||||
},
|
||||
DisableTlsClientAuth: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"basic": {
|
||||
in: []dbWebhook{
|
||||
{
|
||||
Name: "basic",
|
||||
ID: "69350cb6-6c31-4b5e-bf25-affd5053427d",
|
||||
URL: "https://example.com",
|
||||
Kind: "ENRICHING",
|
||||
Secret: "secret",
|
||||
BasicAuth: &dbBasicAuth{
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []*linkedca.Webhook{
|
||||
{
|
||||
Name: "basic",
|
||||
Id: "69350cb6-6c31-4b5e-bf25-affd5053427d",
|
||||
Url: "https://example.com",
|
||||
Kind: linkedca.Webhook_ENRICHING,
|
||||
Secret: "secret",
|
||||
Auth: &linkedca.Webhook_BasicAuth{
|
||||
BasicAuth: &linkedca.BasicAuth{
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := dbWebhooksToLinkedca(tc.in)
|
||||
assert.Equals(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -156,16 +156,17 @@ func NewErrorISE(msg string, args ...interface{}) *Error {
|
|||
|
||||
// WrapError attempts to wrap the internal error.
|
||||
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
|
||||
switch e := err.(type) {
|
||||
case nil:
|
||||
var ee *Error
|
||||
switch {
|
||||
case err == nil:
|
||||
return nil
|
||||
case *Error:
|
||||
if e.Err == nil {
|
||||
e.Err = errors.Errorf(msg+"; "+e.Detail, args...)
|
||||
case errors.As(err, &ee):
|
||||
if ee.Err == nil {
|
||||
ee.Err = errors.Errorf(msg+"; "+ee.Detail, args...)
|
||||
} else {
|
||||
e.Err = errors.Wrapf(e.Err, msg, args...)
|
||||
ee.Err = errors.Wrapf(ee.Err, msg, args...)
|
||||
}
|
||||
return e
|
||||
return ee
|
||||
default:
|
||||
return newError(typ, errors.Wrapf(err, msg, args...))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package authority
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -24,6 +26,7 @@ import (
|
|||
adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql"
|
||||
"github.com/smallstep/certificates/authority/administrator"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/internal/constraints"
|
||||
"github.com/smallstep/certificates/authority/policy"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/cas"
|
||||
|
@ -44,16 +47,18 @@ type Authority struct {
|
|||
adminDB admin.DB
|
||||
templates *templates.Templates
|
||||
linkedCAToken string
|
||||
webhookClient *http.Client
|
||||
|
||||
// X509 CA
|
||||
password []byte
|
||||
issuerPassword []byte
|
||||
x509CAService cas.CertificateAuthorityService
|
||||
rootX509Certs []*x509.Certificate
|
||||
rootX509CertPool *x509.CertPool
|
||||
federatedX509Certs []*x509.Certificate
|
||||
certificates *sync.Map
|
||||
x509Enforcers []provisioner.CertificateEnforcer
|
||||
password []byte
|
||||
issuerPassword []byte
|
||||
x509CAService cas.CertificateAuthorityService
|
||||
rootX509Certs []*x509.Certificate
|
||||
rootX509CertPool *x509.CertPool
|
||||
federatedX509Certs []*x509.Certificate
|
||||
intermediateX509Certs []*x509.Certificate
|
||||
certificates *sync.Map
|
||||
x509Enforcers []provisioner.CertificateEnforcer
|
||||
|
||||
// SCEP CA
|
||||
scepService *scep.Service
|
||||
|
@ -68,7 +73,12 @@ type Authority struct {
|
|||
sshCAUserFederatedCerts []ssh.PublicKey
|
||||
sshCAHostFederatedCerts []ssh.PublicKey
|
||||
|
||||
// Do not re-initialize
|
||||
// CRL vars
|
||||
crlTicker *time.Ticker
|
||||
crlStopper chan struct{}
|
||||
crlMutex sync.Mutex
|
||||
|
||||
// If true, do not re-initialize
|
||||
initOnce bool
|
||||
startTime time.Time
|
||||
|
||||
|
@ -80,13 +90,17 @@ type Authority struct {
|
|||
authorizeRenewFunc provisioner.AuthorizeRenewFunc
|
||||
authorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc
|
||||
|
||||
// Policy engines
|
||||
policyEngine *policy.Engine
|
||||
// Constraints and Policy engines
|
||||
constraintsEngine *constraints.Engine
|
||||
policyEngine *policy.Engine
|
||||
|
||||
adminMutex sync.RWMutex
|
||||
|
||||
// Do Not initialize the authority
|
||||
// If true, do not initialize the authority
|
||||
skipInit bool
|
||||
|
||||
// If true, do not output initialization logs
|
||||
quietInit bool
|
||||
}
|
||||
|
||||
// Info contains information about the authority.
|
||||
|
@ -368,11 +382,17 @@ func (a *Authority) init() error {
|
|||
}
|
||||
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||
SigningKey: a.config.IntermediateKey,
|
||||
Password: []byte(a.password),
|
||||
Password: a.password,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If not defined with an option, add intermediates to the list of
|
||||
// certificates used for name constraints validation at issuance
|
||||
// time.
|
||||
if len(a.intermediateX509Certs) == 0 {
|
||||
a.intermediateX509Certs = append(a.intermediateX509Certs, options.CertificateChain...)
|
||||
}
|
||||
}
|
||||
a.x509CAService, err = cas.New(ctx, options)
|
||||
if err != nil {
|
||||
|
@ -393,13 +413,13 @@ func (a *Authority) init() error {
|
|||
|
||||
// Read root certificates and store them in the certificates map.
|
||||
if len(a.rootX509Certs) == 0 {
|
||||
a.rootX509Certs = make([]*x509.Certificate, len(a.config.Root))
|
||||
for i, path := range a.config.Root {
|
||||
crt, err := pemutil.ReadCertificate(path)
|
||||
a.rootX509Certs = make([]*x509.Certificate, 0, len(a.config.Root))
|
||||
for _, path := range a.config.Root {
|
||||
crts, err := pemutil.ReadCertificateBundle(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.rootX509Certs[i] = crt
|
||||
a.rootX509Certs = append(a.rootX509Certs, crts...)
|
||||
}
|
||||
}
|
||||
for _, crt := range a.rootX509Certs {
|
||||
|
@ -414,13 +434,13 @@ func (a *Authority) init() error {
|
|||
|
||||
// Read federated certificates and store them in the certificates map.
|
||||
if len(a.federatedX509Certs) == 0 {
|
||||
a.federatedX509Certs = make([]*x509.Certificate, len(a.config.FederatedRoots))
|
||||
for i, path := range a.config.FederatedRoots {
|
||||
crt, err := pemutil.ReadCertificate(path)
|
||||
a.federatedX509Certs = make([]*x509.Certificate, 0, len(a.config.FederatedRoots))
|
||||
for _, path := range a.config.FederatedRoots {
|
||||
crts, err := pemutil.ReadCertificateBundle(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.federatedX509Certs[i] = crt
|
||||
a.federatedX509Certs = append(a.federatedX509Certs, crts...)
|
||||
}
|
||||
}
|
||||
for _, crt := range a.federatedX509Certs {
|
||||
|
@ -434,7 +454,7 @@ func (a *Authority) init() error {
|
|||
if a.config.SSH.HostKey != "" {
|
||||
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||
SigningKey: a.config.SSH.HostKey,
|
||||
Password: []byte(a.sshHostPassword),
|
||||
Password: a.sshHostPassword,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -460,7 +480,7 @@ func (a *Authority) init() error {
|
|||
if a.config.SSH.UserKey != "" {
|
||||
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||
SigningKey: a.config.SSH.UserKey,
|
||||
Password: []byte(a.sshUserPassword),
|
||||
Password: a.sshUserPassword,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -525,6 +545,101 @@ func (a *Authority) init() error {
|
|||
tmplVars.SSH.UserFederatedKeys = append(tmplVars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts...)
|
||||
}
|
||||
|
||||
if a.config.AuthorityConfig.EnableAdmin {
|
||||
// Initialize step-ca Admin Database if it's not already initialized using
|
||||
// WithAdminDB.
|
||||
if a.adminDB == nil {
|
||||
if linkedcaClient != nil {
|
||||
a.adminDB = linkedcaClient
|
||||
} else {
|
||||
a.adminDB, err = adminDBNosql.New(a.db.(nosql.DB), admin.DefaultAuthorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provs, err := a.adminDB.GetProvisioners(ctx)
|
||||
if err != nil {
|
||||
return admin.WrapErrorISE(err, "error loading provisioners to initialize authority")
|
||||
}
|
||||
if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") {
|
||||
// Migration will currently only be kicked off once, because either one or more provisioners
|
||||
// are migrated or a default JWK provisioner will be created in the DB. It won't run for
|
||||
// linked or hosted deployments. Not for linked, because that case is explicitly checked
|
||||
// for above. Not for hosted, because there'll be at least an existing OIDC provisioner.
|
||||
var firstJWKProvisioner *linkedca.Provisioner
|
||||
if len(a.config.AuthorityConfig.Provisioners) > 0 {
|
||||
// Existing provisioners detected; try migrating them to DB storage.
|
||||
a.initLogf("Starting migration of provisioners")
|
||||
for _, p := range a.config.AuthorityConfig.Provisioners {
|
||||
lp, err := ProvisionerToLinkedca(p)
|
||||
if err != nil {
|
||||
return admin.WrapErrorISE(err, "error transforming provisioner %q while migrating", p.GetName())
|
||||
}
|
||||
|
||||
// Store the provisioner to be migrated
|
||||
if err := a.adminDB.CreateProvisioner(ctx, lp); err != nil {
|
||||
return admin.WrapErrorISE(err, "error creating provisioner %q while migrating", p.GetName())
|
||||
}
|
||||
|
||||
// Mark the first JWK provisioner, so that it can be used for administration purposes
|
||||
if firstJWKProvisioner == nil && lp.Type == linkedca.Provisioner_JWK {
|
||||
firstJWKProvisioner = lp
|
||||
a.initLogf("Migrated JWK provisioner %q with admin permissions", p.GetName())
|
||||
} else {
|
||||
a.initLogf("Migrated %s provisioner %q", p.GetType(), p.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
c := a.config
|
||||
if c.WasLoadedFromFile() {
|
||||
// The provisioners in the configuration file can be deleted from
|
||||
// the file by editing it. Automatic rewriting of the file was considered
|
||||
// to be too surprising for users and not the right solution for all
|
||||
// use cases, so we leave it up to users to this themselves.
|
||||
a.initLogf("Provisioners that were migrated can now be removed from `ca.json` by editing it")
|
||||
}
|
||||
|
||||
a.initLogf("Finished migrating provisioners")
|
||||
}
|
||||
|
||||
// Create first JWK provisioner for remote administration purposes if none exists yet
|
||||
if firstJWKProvisioner == nil {
|
||||
firstJWKProvisioner, err = CreateFirstProvisioner(ctx, a.adminDB, string(a.password))
|
||||
if err != nil {
|
||||
return admin.WrapErrorISE(err, "error creating first provisioner")
|
||||
}
|
||||
a.initLogf("Created JWK provisioner %q with admin permissions", firstJWKProvisioner.GetName())
|
||||
}
|
||||
|
||||
// Create first super admin, belonging to the first JWK provisioner
|
||||
// TODO(hs): pass a user-provided first super admin subject to here. With `ca init` it's
|
||||
// added to the DB immediately if using remote management. But when migrating from
|
||||
// ca.json to the DB, this option doesn't exist. Adding a flag just to do it during
|
||||
// migration isn't nice. We could opt for a user to change it afterwards. There exist
|
||||
// cases in which creation of `step` could lock out a user from API access. This is the
|
||||
// case if `step` isn't allowed to be signed by Name Constraints or the X.509 policy.
|
||||
// We have protection for that when creating and updating a policy, but if a policy or
|
||||
// Name Constraints are in use at the time of migration, that could lock the user out.
|
||||
superAdminSubject := "step"
|
||||
if err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{
|
||||
ProvisionerId: firstJWKProvisioner.Id,
|
||||
Subject: superAdminSubject,
|
||||
Type: linkedca.Admin_SUPER_ADMIN,
|
||||
}); err != nil {
|
||||
return admin.WrapErrorISE(err, "error creating first admin")
|
||||
}
|
||||
|
||||
a.initLogf("Created super admin %q for JWK provisioner %q", superAdminSubject, firstJWKProvisioner.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
// Load Provisioners and Admins
|
||||
if err := a.ReloadAdminResources(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if a KMS with decryption capability is required and available
|
||||
if a.requiresDecrypter() {
|
||||
if _, ok := a.keyManager.(kmsapi.Decrypter); !ok {
|
||||
|
@ -545,7 +660,7 @@ func (a *Authority) init() error {
|
|||
options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...)
|
||||
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||
SigningKey: a.config.IntermediateKey,
|
||||
Password: []byte(a.password),
|
||||
Password: a.password,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -554,7 +669,7 @@ func (a *Authority) init() error {
|
|||
if km, ok := a.keyManager.(kmsapi.Decrypter); ok {
|
||||
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
|
||||
DecryptionKey: a.config.IntermediateKey,
|
||||
Password: []byte(a.password),
|
||||
Password: a.password,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -569,45 +684,19 @@ func (a *Authority) init() error {
|
|||
// TODO: mimick the x509CAService GetCertificateAuthority here too?
|
||||
}
|
||||
|
||||
if a.config.AuthorityConfig.EnableAdmin {
|
||||
// Initialize step-ca Admin Database if it's not already initialized using
|
||||
// WithAdminDB.
|
||||
if a.adminDB == nil {
|
||||
if linkedcaClient != nil {
|
||||
a.adminDB = linkedcaClient
|
||||
} else {
|
||||
a.adminDB, err = adminDBNosql.New(a.db.(nosql.DB), admin.DefaultAuthorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Load X509 constraints engine.
|
||||
//
|
||||
// This is currently only available in CA mode.
|
||||
if size := len(a.intermediateX509Certs); size > 0 {
|
||||
last := a.intermediateX509Certs[size-1]
|
||||
constraintCerts := make([]*x509.Certificate, 0, size+1)
|
||||
constraintCerts = append(constraintCerts, a.intermediateX509Certs...)
|
||||
for _, root := range a.rootX509Certs {
|
||||
if bytes.Equal(last.RawIssuer, root.RawSubject) && bytes.Equal(last.AuthorityKeyId, root.SubjectKeyId) {
|
||||
constraintCerts = append(constraintCerts, root)
|
||||
}
|
||||
}
|
||||
|
||||
provs, err := a.adminDB.GetProvisioners(ctx)
|
||||
if err != nil {
|
||||
return admin.WrapErrorISE(err, "error loading provisioners to initialize authority")
|
||||
}
|
||||
if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") {
|
||||
// Create First Provisioner
|
||||
prov, err := CreateFirstProvisioner(ctx, a.adminDB, string(a.password))
|
||||
if err != nil {
|
||||
return admin.WrapErrorISE(err, "error creating first provisioner")
|
||||
}
|
||||
|
||||
// Create first admin
|
||||
if err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{
|
||||
ProvisionerId: prov.Id,
|
||||
Subject: "step",
|
||||
Type: linkedca.Admin_SUPER_ADMIN,
|
||||
}); err != nil {
|
||||
return admin.WrapErrorISE(err, "error creating first admin")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load Provisioners and Admins
|
||||
if err := a.ReloadAdminResources(ctx); err != nil {
|
||||
return err
|
||||
a.constraintsEngine = constraints.New(constraintCerts...)
|
||||
}
|
||||
|
||||
// Load x509 and SSH Policy Engines
|
||||
|
@ -627,6 +716,18 @@ func (a *Authority) init() error {
|
|||
a.templates.Data["Step"] = tmplVars
|
||||
}
|
||||
|
||||
// Start the CRL generator, we can assume the configuration is validated.
|
||||
if a.config.CRL.IsEnabled() {
|
||||
// Default cache duration to the default one
|
||||
if v := a.config.CRL.CacheDuration; v == nil || v.Duration <= 0 {
|
||||
a.config.CRL.CacheDuration = config.DefaultCRLCacheDuration
|
||||
}
|
||||
// Start CRL generator
|
||||
if err := a.startCRLGenerator(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// JWT numeric dates are seconds.
|
||||
a.startTime = time.Now().Truncate(time.Second)
|
||||
// Set flag indicating that initialization has been completed, and should
|
||||
|
@ -636,6 +737,14 @@ func (a *Authority) init() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// initLogf is used to log initialization information. The output
|
||||
// can be disabled by starting the CA with the `--quiet` flag.
|
||||
func (a *Authority) initLogf(format string, v ...any) {
|
||||
if !a.quietInit {
|
||||
log.Printf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// GetID returns the define authority id or a zero uuid.
|
||||
func (a *Authority) GetID() string {
|
||||
const zeroUUID = "00000000-0000-0000-0000-000000000000"
|
||||
|
@ -685,6 +794,11 @@ func (a *Authority) IsAdminAPIEnabled() bool {
|
|||
|
||||
// Shutdown safely shuts down any clients, databases, etc. held by the Authority.
|
||||
func (a *Authority) Shutdown() error {
|
||||
if a.crlTicker != nil {
|
||||
a.crlTicker.Stop()
|
||||
close(a.crlStopper)
|
||||
}
|
||||
|
||||
if err := a.keyManager.Close(); err != nil {
|
||||
log.Printf("error closing the key manager: %v", err)
|
||||
}
|
||||
|
@ -693,6 +807,11 @@ func (a *Authority) Shutdown() error {
|
|||
|
||||
// CloseForReload closes internal services, to allow a safe reload.
|
||||
func (a *Authority) CloseForReload() {
|
||||
if a.crlTicker != nil {
|
||||
a.crlTicker.Stop()
|
||||
close(a.crlStopper)
|
||||
}
|
||||
|
||||
if err := a.keyManager.Close(); err != nil {
|
||||
log.Printf("error closing the key manager: %v", err)
|
||||
}
|
||||
|
@ -733,11 +852,49 @@ func (a *Authority) requiresSCEPService() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// GetSCEPService returns the configured SCEP Service
|
||||
// TODO: this function is intended to exist temporarily
|
||||
// in order to make SCEP work more easily. It can be
|
||||
// made more correct by using the right interfaces/abstractions
|
||||
// after it works as expected.
|
||||
// GetSCEPService returns the configured SCEP Service.
|
||||
//
|
||||
// TODO: this function is intended to exist temporarily in order to make SCEP
|
||||
// work more easily. It can be made more correct by using the right
|
||||
// interfaces/abstractions after it works as expected.
|
||||
func (a *Authority) GetSCEPService() *scep.Service {
|
||||
return a.scepService
|
||||
}
|
||||
|
||||
func (a *Authority) startCRLGenerator() error {
|
||||
if !a.config.CRL.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check that there is a valid CRL in the DB right now. If it doesn't exist
|
||||
// or is expired, generate one now
|
||||
_, ok := a.db.(db.CertificateRevocationListDB)
|
||||
if !ok {
|
||||
return errors.Errorf("CRL Generation requested, but database does not support CRL generation")
|
||||
}
|
||||
|
||||
// Always create a new CRL on startup in case the CA has been down and the
|
||||
// time to next expected CRL update is less than the cache duration.
|
||||
if err := a.GenerateCertificateRevocationList(); err != nil {
|
||||
return errors.Wrap(err, "could not generate a CRL")
|
||||
}
|
||||
|
||||
a.crlStopper = make(chan struct{}, 1)
|
||||
a.crlTicker = time.NewTicker(a.config.CRL.TickerDuration())
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-a.crlTicker.C:
|
||||
log.Println("Regenerating CRL")
|
||||
if err := a.GenerateCertificateRevocationList(); err != nil {
|
||||
log.Printf("error regenerating the CRL: %v", err)
|
||||
}
|
||||
case <-a.crlStopper:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -6,8 +6,10 @@ import (
|
|||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -18,6 +20,7 @@ import (
|
|||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/minica"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
)
|
||||
|
||||
|
@ -172,6 +175,130 @@ func TestAuthorityNew(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAuthorityNew_bundles(t *testing.T) {
|
||||
ca0, err := minica.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ca1, err := minica.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ca2, err := minica.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootPath := t.TempDir()
|
||||
writeCert := func(fn string, certs ...*x509.Certificate) error {
|
||||
var b []byte
|
||||
for _, crt := range certs {
|
||||
b = append(b, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: crt.Raw,
|
||||
})...)
|
||||
}
|
||||
return os.WriteFile(filepath.Join(rootPath, fn), b, 0600)
|
||||
}
|
||||
writeKey := func(fn string, signer crypto.Signer) error {
|
||||
_, err := pemutil.Serialize(signer, pemutil.ToFile(filepath.Join(rootPath, fn), 0600))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writeCert("root0.crt", ca0.Root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeCert("int0.crt", ca0.Intermediate); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeKey("int0.key", ca0.Signer); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeCert("root1.crt", ca1.Root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeCert("int1.crt", ca1.Intermediate); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeKey("int1.key", ca1.Signer); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeCert("bundle0.crt", ca0.Root, ca1.Root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeCert("bundle1.crt", ca1.Root, ca2.Root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config *config.Config
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok ca0", &config.Config{
|
||||
Address: "127.0.0.1:443",
|
||||
Root: []string{filepath.Join(rootPath, "root0.crt")},
|
||||
IntermediateCert: filepath.Join(rootPath, "int0.crt"),
|
||||
IntermediateKey: filepath.Join(rootPath, "int0.key"),
|
||||
DNSNames: []string{"127.0.0.1"},
|
||||
AuthorityConfig: &AuthConfig{},
|
||||
}, false},
|
||||
{"ok bundle", &config.Config{
|
||||
Address: "127.0.0.1:443",
|
||||
Root: []string{filepath.Join(rootPath, "bundle0.crt")},
|
||||
IntermediateCert: filepath.Join(rootPath, "int0.crt"),
|
||||
IntermediateKey: filepath.Join(rootPath, "int0.key"),
|
||||
DNSNames: []string{"127.0.0.1"},
|
||||
AuthorityConfig: &AuthConfig{},
|
||||
}, false},
|
||||
{"ok federated ca1", &config.Config{
|
||||
Address: "127.0.0.1:443",
|
||||
Root: []string{filepath.Join(rootPath, "root0.crt")},
|
||||
FederatedRoots: []string{filepath.Join(rootPath, "root1.crt")},
|
||||
IntermediateCert: filepath.Join(rootPath, "int0.crt"),
|
||||
IntermediateKey: filepath.Join(rootPath, "int0.key"),
|
||||
DNSNames: []string{"127.0.0.1"},
|
||||
AuthorityConfig: &AuthConfig{},
|
||||
}, false},
|
||||
{"ok federated bundle", &config.Config{
|
||||
Address: "127.0.0.1:443",
|
||||
Root: []string{filepath.Join(rootPath, "root0.crt")},
|
||||
FederatedRoots: []string{filepath.Join(rootPath, "bundle1.crt")},
|
||||
IntermediateCert: filepath.Join(rootPath, "int0.crt"),
|
||||
IntermediateKey: filepath.Join(rootPath, "int0.key"),
|
||||
DNSNames: []string{"127.0.0.1"},
|
||||
AuthorityConfig: &AuthConfig{},
|
||||
}, false},
|
||||
{"fail root", &config.Config{
|
||||
Address: "127.0.0.1:443",
|
||||
Root: []string{filepath.Join(rootPath, "missing.crt")},
|
||||
IntermediateCert: filepath.Join(rootPath, "int0.crt"),
|
||||
IntermediateKey: filepath.Join(rootPath, "int0.key"),
|
||||
DNSNames: []string{"127.0.0.1"},
|
||||
AuthorityConfig: &AuthConfig{},
|
||||
}, true},
|
||||
{"fail federated", &config.Config{
|
||||
Address: "127.0.0.1:443",
|
||||
Root: []string{filepath.Join(rootPath, "root0.crt")},
|
||||
FederatedRoots: []string{filepath.Join(rootPath, "missing.crt")},
|
||||
IntermediateCert: filepath.Join(rootPath, "int0.crt"),
|
||||
IntermediateKey: filepath.Join(rootPath, "int0.key"),
|
||||
DNSNames: []string{"127.0.0.1"},
|
||||
AuthorityConfig: &AuthConfig{},
|
||||
}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := New(tt.config)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthority_GetDatabase(t *testing.T) {
|
||||
auth := testAuthority(t)
|
||||
authWithDatabase, err := New(auth.config, WithDatabase(auth.db))
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
|
@ -285,7 +286,7 @@ func (a *Authority) authorizeRevoke(ctx context.Context, token string) error {
|
|||
// extra extension cannot be found, authorize the renewal by default.
|
||||
//
|
||||
// TODO(mariano): should we authorize by default?
|
||||
func (a *Authority) authorizeRenew(cert *x509.Certificate) error {
|
||||
func (a *Authority) authorizeRenew(ctx context.Context, cert *x509.Certificate) error {
|
||||
serial := cert.SerialNumber.String()
|
||||
var opts = []interface{}{errs.WithKeyVal("serialNumber", serial)}
|
||||
|
||||
|
@ -307,14 +308,14 @@ func (a *Authority) authorizeRenew(cert *x509.Certificate) error {
|
|||
return errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...)
|
||||
}
|
||||
}
|
||||
if err := p.AuthorizeRenew(context.Background(), cert); err != nil {
|
||||
if err := p.AuthorizeRenew(ctx, cert); err != nil {
|
||||
return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// authorizeSSHCertificate returns an error if the given certificate is revoked.
|
||||
func (a *Authority) authorizeSSHCertificate(ctx context.Context, cert *ssh.Certificate) error {
|
||||
func (a *Authority) authorizeSSHCertificate(_ context.Context, cert *ssh.Certificate) error {
|
||||
var err error
|
||||
var isRevoked bool
|
||||
|
||||
|
@ -393,7 +394,7 @@ func (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error
|
|||
|
||||
// AuthorizeRenewToken validates the renew token and returns the leaf
|
||||
// certificate in the x5cInsecure header.
|
||||
func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.Certificate, error) {
|
||||
func (a *Authority) AuthorizeRenewToken(_ context.Context, ott string) (*x509.Certificate, error) {
|
||||
var claims jose.Claims
|
||||
jwt, chain, err := jose.ParseX5cInsecure(ott, a.rootX509Certs)
|
||||
if err != nil {
|
||||
|
@ -416,16 +417,16 @@ func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.
|
|||
Subject: leaf.Subject.CommonName,
|
||||
Time: time.Now().UTC(),
|
||||
}, time.Minute); err != nil {
|
||||
switch err {
|
||||
case jose.ErrInvalidIssuer:
|
||||
switch {
|
||||
case errors.Is(err, jose.ErrInvalidIssuer):
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid issuer claim (iss)"))
|
||||
case jose.ErrInvalidSubject:
|
||||
case errors.Is(err, jose.ErrInvalidSubject):
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid subject claim (sub)"))
|
||||
case jose.ErrNotValidYet:
|
||||
case errors.Is(err, jose.ErrNotValidYet):
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token not valid yet (nbf)"))
|
||||
case jose.ErrExpired:
|
||||
case errors.Is(err, jose.ErrExpired):
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token is expired (exp)"))
|
||||
case jose.ErrIssuedInTheFuture:
|
||||
case errors.Is(err, jose.ErrIssuedInTheFuture):
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token issued in the future (iat)"))
|
||||
default:
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token"))
|
||||
|
@ -433,7 +434,7 @@ func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.
|
|||
}
|
||||
|
||||
audiences := a.config.GetAudiences().Renew
|
||||
if !matchesAudience(claims.Audience, audiences) {
|
||||
if !matchesAudience(claims.Audience, audiences) && !isRAProvisioner(p) {
|
||||
return nil, errs.InternalServerErr(jose.ErrInvalidAudience, errs.WithMessage("error validating renew token: invalid audience claim (aud)"))
|
||||
}
|
||||
|
||||
|
|
|
@ -313,8 +313,8 @@ func TestAuthority_authorizeToken(t *testing.T) {
|
|||
p, err := tc.auth.authorizeToken(context.Background(), tc.token)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -399,8 +399,8 @@ func TestAuthority_authorizeRevoke(t *testing.T) {
|
|||
|
||||
if err := tc.auth.authorizeRevoke(context.Background(), tc.token); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -484,14 +484,14 @@ func TestAuthority_authorizeSign(t *testing.T) {
|
|||
got, err := tc.auth.authorizeSign(context.Background(), tc.token)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, 9, len(got)) // number of provisioner.SignOptions returned
|
||||
assert.Equals(t, 10, len(got)) // number of provisioner.SignOptions returned
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -743,13 +743,13 @@ func TestAuthority_Authorize(t *testing.T) {
|
|||
if err != nil {
|
||||
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
|
||||
assert.Nil(t, got)
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
|
||||
ctxErr, ok := err.(*errs.Error)
|
||||
assert.Fatal(t, ok, "error is not of type *errs.Error")
|
||||
var ctxErr *errs.Error
|
||||
assert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
|
||||
assert.Equals(t, ctxErr.Details["token"], tc.token)
|
||||
}
|
||||
} else {
|
||||
|
@ -876,16 +876,16 @@ func TestAuthority_authorizeRenew(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
tc := genTestCase(t)
|
||||
|
||||
err := tc.auth.authorizeRenew(tc.cert)
|
||||
err := tc.auth.authorizeRenew(context.Background(), tc.cert)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
|
||||
ctxErr, ok := err.(*errs.Error)
|
||||
assert.Fatal(t, ok, "error is not of type *errs.Error")
|
||||
var ctxErr *errs.Error
|
||||
assert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
|
||||
assert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String())
|
||||
}
|
||||
} else {
|
||||
|
@ -1027,14 +1027,14 @@ func TestAuthority_authorizeSSHSign(t *testing.T) {
|
|||
got, err := tc.auth.authorizeSSHSign(context.Background(), tc.token)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Len(t, 9, got) // number of provisioner.SignOptions returned
|
||||
assert.Len(t, 10, got) // number of provisioner.SignOptions returned
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1144,8 +1144,8 @@ func TestAuthority_authorizeSSHRenew(t *testing.T) {
|
|||
got, err := tc.auth.authorizeSSHRenew(context.Background(), tc.token)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -1244,8 +1244,8 @@ func TestAuthority_authorizeSSHRevoke(t *testing.T) {
|
|||
|
||||
if err := tc.auth.authorizeSSHRevoke(context.Background(), tc.token); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -1337,8 +1337,8 @@ func TestAuthority_authorizeSSHRekey(t *testing.T) {
|
|||
cert, signOpts, err := tc.auth.authorizeSSHRekey(context.Background(), tc.token)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -1459,6 +1459,37 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) {
|
|||
})
|
||||
return nil
|
||||
}))
|
||||
a4 := testAuthority(t)
|
||||
a4.db = &db.MockAuthDB{
|
||||
MUseToken: func(id, tok string) (bool, error) {
|
||||
return true, nil
|
||||
},
|
||||
MGetCertificateData: func(serialNumber string) (*db.CertificateData, error) {
|
||||
return &db.CertificateData{
|
||||
Provisioner: &db.ProvisionerData{ID: "Max:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk", Name: "Max"},
|
||||
RaInfo: &provisioner.RAInfo{ProvisionerName: "ra"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
t4, c4 := generateX5cToken(a1, signer, jose.Claims{
|
||||
Audience: []string{"https://ra.example.com/1.0/renew"},
|
||||
Subject: "test.example.com",
|
||||
Issuer: "step-ca-client/1.0",
|
||||
NotBefore: jose.NewNumericDate(now),
|
||||
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {
|
||||
cert.NotBefore = now
|
||||
cert.NotAfter = now.Add(time.Hour)
|
||||
b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},
|
||||
Value: b,
|
||||
})
|
||||
return nil
|
||||
}))
|
||||
badSigner, _ := generateX5cToken(a1, otherSigner, jose.Claims{
|
||||
Audience: []string{"https://example.com/1.0/renew"},
|
||||
Subject: "test.example.com",
|
||||
|
@ -1627,6 +1658,7 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) {
|
|||
{"ok", a1, args{ctx, t1}, c1, false},
|
||||
{"ok expired cert", a1, args{ctx, t2}, c2, false},
|
||||
{"ok provisioner issuer", a1, args{ctx, t3}, c3, false},
|
||||
{"ok ra provisioner", a4, args{ctx, t4}, c4, false},
|
||||
{"fail token", a1, args{ctx, "not.a.token"}, nil, true},
|
||||
{"fail token reuse", a1, args{ctx, t1}, nil, true},
|
||||
{"fail token signature", a1, args{ctx, badSigner}, nil, true},
|
||||
|
|
|
@ -35,8 +35,13 @@ var (
|
|||
// DefaultEnableSSHCA enable SSH CA features per provisioner or globally
|
||||
// for all provisioners.
|
||||
DefaultEnableSSHCA = false
|
||||
// GlobalProvisionerClaims default claims for the Authority. Can be overridden
|
||||
// by provisioner specific claims.
|
||||
// DefaultCRLCacheDuration is the default cache duration for the CRL.
|
||||
DefaultCRLCacheDuration = &provisioner.Duration{Duration: 24 * time.Hour}
|
||||
// DefaultCRLExpiredDuration is the default duration in which expired
|
||||
// certificates will remain in the CRL after expiration.
|
||||
DefaultCRLExpiredDuration = time.Hour
|
||||
// GlobalProvisionerClaims is the default duration that expired certificates
|
||||
// remain in the CRL after expiration.
|
||||
GlobalProvisionerClaims = provisioner.Claims{
|
||||
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs
|
||||
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
||||
|
@ -72,7 +77,62 @@ type Config struct {
|
|||
Password string `json:"password,omitempty"`
|
||||
Templates *templates.Templates `json:"templates,omitempty"`
|
||||
CommonName string `json:"commonName,omitempty"`
|
||||
CRL *CRLConfig `json:"crl,omitempty"`
|
||||
SkipValidation bool `json:"-"`
|
||||
NNSServer string `json:"nnsServer,omitempty"`
|
||||
|
||||
// Keeps record of the filename the Config is read from
|
||||
loadedFromFilepath string
|
||||
}
|
||||
|
||||
// CRLConfig represents config options for CRL generation
|
||||
type CRLConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
GenerateOnRevoke bool `json:"generateOnRevoke,omitempty"`
|
||||
CacheDuration *provisioner.Duration `json:"cacheDuration,omitempty"`
|
||||
RenewPeriod *provisioner.Duration `json:"renewPeriod,omitempty"`
|
||||
IDPurl string `json:"idpURL,omitempty"`
|
||||
}
|
||||
|
||||
// IsEnabled returns if the CRL is enabled.
|
||||
func (c *CRLConfig) IsEnabled() bool {
|
||||
return c != nil && c.Enabled
|
||||
}
|
||||
|
||||
// Validate validates the CRL configuration.
|
||||
func (c *CRLConfig) Validate() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.CacheDuration != nil && c.CacheDuration.Duration < 0 {
|
||||
return errors.New("crl.cacheDuration must be greater than or equal to 0")
|
||||
}
|
||||
|
||||
if c.RenewPeriod != nil && c.RenewPeriod.Duration < 0 {
|
||||
return errors.New("crl.renewPeriod must be greater than or equal to 0")
|
||||
}
|
||||
|
||||
if c.RenewPeriod != nil && c.CacheDuration != nil &&
|
||||
c.RenewPeriod.Duration > c.CacheDuration.Duration {
|
||||
return errors.New("crl.cacheDuration must be greater than or equal to crl.renewPeriod")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TickerDuration the renewal ticker duration. This is set by renewPeriod, of it
|
||||
// is not set is ~2/3 of cacheDuration.
|
||||
func (c *CRLConfig) TickerDuration() time.Duration {
|
||||
if !c.IsEnabled() {
|
||||
return 0
|
||||
}
|
||||
|
||||
if c.RenewPeriod != nil && c.RenewPeriod.Duration > 0 {
|
||||
return c.RenewPeriod.Duration
|
||||
}
|
||||
|
||||
return (c.CacheDuration.Duration / 3) * 2
|
||||
}
|
||||
|
||||
// ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer
|
||||
|
@ -123,7 +183,7 @@ func (c *AuthConfig) init() {
|
|||
}
|
||||
|
||||
// Validate validates the authority configuration.
|
||||
func (c *AuthConfig) Validate(audiences provisioner.Audiences) error {
|
||||
func (c *AuthConfig) Validate(provisioner.Audiences) error {
|
||||
if c == nil {
|
||||
return errors.New("authority cannot be undefined")
|
||||
}
|
||||
|
@ -163,6 +223,10 @@ func LoadConfiguration(filename string) (*Config, error) {
|
|||
return nil, errors.Wrapf(err, "error parsing %s", filename)
|
||||
}
|
||||
|
||||
// store filename that was read to populate Config
|
||||
c.loadedFromFilepath = filename
|
||||
|
||||
// initialize the Config
|
||||
c.Init()
|
||||
|
||||
return &c, nil
|
||||
|
@ -183,6 +247,9 @@ func (c *Config) Init() {
|
|||
if c.CommonName == "" {
|
||||
c.CommonName = "Step Online CA"
|
||||
}
|
||||
if c.CRL != nil && c.CRL.Enabled && c.CRL.CacheDuration == nil {
|
||||
c.CRL.CacheDuration = DefaultCRLCacheDuration
|
||||
}
|
||||
c.AuthorityConfig.init()
|
||||
}
|
||||
|
||||
|
@ -199,6 +266,30 @@ func (c *Config) Save(filename string) error {
|
|||
return errors.Wrapf(enc.Encode(c), "error writing %s", filename)
|
||||
}
|
||||
|
||||
// Commit saves the current configuration to the same
|
||||
// file it was initially loaded from.
|
||||
//
|
||||
// TODO(hs): rename Save() to WriteTo() and replace this
|
||||
// with Save()? Or is Commit clear enough.
|
||||
func (c *Config) Commit() error {
|
||||
if !c.WasLoadedFromFile() {
|
||||
return errors.New("cannot commit configuration if not loaded from file")
|
||||
}
|
||||
return c.Save(c.loadedFromFilepath)
|
||||
}
|
||||
|
||||
// WasLoadedFromFile returns whether or not the Config was
|
||||
// loaded from a file.
|
||||
func (c *Config) WasLoadedFromFile() bool {
|
||||
return c.loadedFromFilepath != ""
|
||||
}
|
||||
|
||||
// Filepath returns the path to the file the Config was
|
||||
// loaded from.
|
||||
func (c *Config) Filepath() string {
|
||||
return c.loadedFromFilepath
|
||||
}
|
||||
|
||||
// Validate validates the configuration.
|
||||
func (c *Config) Validate() error {
|
||||
switch {
|
||||
|
@ -269,6 +360,11 @@ func (c *Config) Validate() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Validate crl config: nil is ok
|
||||
if err := c.CRL.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.AuthorityConfig.Validate(c.GetAudiences())
|
||||
}
|
||||
|
||||
|
|
|
@ -169,7 +169,7 @@ func (t *TLSOptions) TLSConfig() *tls.Config {
|
|||
rs = tls.RenegotiateNever
|
||||
}
|
||||
|
||||
// nolint:gosec // default MinVersion 1.2, if defined but empty 1.3 is used
|
||||
//nolint:gosec // default MinVersion 1.2, if defined but empty 1.3 is used
|
||||
return &tls.Config{
|
||||
CipherSuites: t.CipherSuites.Value(),
|
||||
MinVersion: t.MinVersion.Value(),
|
||||
|
|
135
authority/internal/constraints/constraints.go
Normal file
135
authority/internal/constraints/constraints.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package constraints
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
||||
// ConstraintError is the typed error that will be returned if a constraint
|
||||
// error is found.
|
||||
type ConstraintError struct {
|
||||
Type string
|
||||
Name string
|
||||
Detail string
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e ConstraintError) Error() string {
|
||||
return e.Detail
|
||||
}
|
||||
|
||||
// As implements the As(any) bool interface and allows to use "errors.As()" to
|
||||
// convert the ConstraintError to an errs.Error.
|
||||
func (e ConstraintError) As(v any) bool {
|
||||
if err, ok := v.(**errs.Error); ok {
|
||||
*err = &errs.Error{
|
||||
Status: http.StatusForbidden,
|
||||
Msg: e.Detail,
|
||||
Err: e,
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Engine implements a constraint validator for DNS names, IP addresses, Email
|
||||
// addresses and URIs.
|
||||
type Engine struct {
|
||||
hasNameConstraints bool
|
||||
permittedDNSDomains []string
|
||||
excludedDNSDomains []string
|
||||
permittedIPRanges []*net.IPNet
|
||||
excludedIPRanges []*net.IPNet
|
||||
permittedEmailAddresses []string
|
||||
excludedEmailAddresses []string
|
||||
permittedURIDomains []string
|
||||
excludedURIDomains []string
|
||||
}
|
||||
|
||||
// New creates a constraint validation engine that contains the given chain of
|
||||
// certificates.
|
||||
func New(chain ...*x509.Certificate) *Engine {
|
||||
e := new(Engine)
|
||||
for _, crt := range chain {
|
||||
e.permittedDNSDomains = append(e.permittedDNSDomains, crt.PermittedDNSDomains...)
|
||||
e.excludedDNSDomains = append(e.excludedDNSDomains, crt.ExcludedDNSDomains...)
|
||||
e.permittedIPRanges = append(e.permittedIPRanges, crt.PermittedIPRanges...)
|
||||
e.excludedIPRanges = append(e.excludedIPRanges, crt.ExcludedIPRanges...)
|
||||
e.permittedEmailAddresses = append(e.permittedEmailAddresses, crt.PermittedEmailAddresses...)
|
||||
e.excludedEmailAddresses = append(e.excludedEmailAddresses, crt.ExcludedEmailAddresses...)
|
||||
e.permittedURIDomains = append(e.permittedURIDomains, crt.PermittedURIDomains...)
|
||||
e.excludedURIDomains = append(e.excludedURIDomains, crt.ExcludedURIDomains...)
|
||||
}
|
||||
|
||||
e.hasNameConstraints = len(e.permittedDNSDomains) > 0 || len(e.excludedDNSDomains) > 0 ||
|
||||
len(e.permittedIPRanges) > 0 || len(e.excludedIPRanges) > 0 ||
|
||||
len(e.permittedEmailAddresses) > 0 || len(e.excludedEmailAddresses) > 0 ||
|
||||
len(e.permittedURIDomains) > 0 || len(e.excludedURIDomains) > 0
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// Validate checks the given names with the name constraints defined in the
|
||||
// service.
|
||||
func (e *Engine) Validate(dnsNames []string, ipAddresses []net.IP, emailAddresses []string, uris []*url.URL) error {
|
||||
if e == nil || !e.hasNameConstraints {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, name := range dnsNames {
|
||||
if err := checkNameConstraints("DNS name", name, name, e.permittedDNSDomains, e.excludedDNSDomains,
|
||||
func(parsedName, constraint any) (bool, error) {
|
||||
return matchDomainConstraint(parsedName.(string), constraint.(string))
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, ip := range ipAddresses {
|
||||
if err := checkNameConstraints("IP address", ip.String(), ip, e.permittedIPRanges, e.excludedIPRanges,
|
||||
func(parsedName, constraint any) (bool, error) {
|
||||
return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet))
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, email := range emailAddresses {
|
||||
mailbox, ok := parseRFC2821Mailbox(email)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot parse rfc822Name %q", email)
|
||||
}
|
||||
if err := checkNameConstraints("Email address", email, mailbox, e.permittedEmailAddresses, e.excludedEmailAddresses,
|
||||
func(parsedName, constraint any) (bool, error) {
|
||||
return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string))
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, uri := range uris {
|
||||
if err := checkNameConstraints("URI", uri.String(), uri, e.permittedURIDomains, e.excludedURIDomains,
|
||||
func(parsedName, constraint any) (bool, error) {
|
||||
return matchURIConstraint(parsedName.(*url.URL), constraint.(string))
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateCertificate validates the DNS names, IP addresses, Email addresses
|
||||
// and URIs present in the given certificate.
|
||||
func (e *Engine) ValidateCertificate(cert *x509.Certificate) error {
|
||||
return e.Validate(cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs)
|
||||
}
|
334
authority/internal/constraints/constraints_test.go
Normal file
334
authority/internal/constraints/constraints_test.go
Normal file
|
@ -0,0 +1,334 @@
|
|||
package constraints
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go.step.sm/crypto/minica"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
ca1, err := minica.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ca2, err := minica.New(
|
||||
minica.WithIntermediateTemplate(`{
|
||||
"subject": {{ toJson .Subject }},
|
||||
"keyUsage": ["certSign", "crlSign"],
|
||||
"basicConstraints": {
|
||||
"isCA": true,
|
||||
"maxPathLen": 0
|
||||
},
|
||||
"nameConstraints": {
|
||||
"critical": true,
|
||||
"permittedDNSDomains": ["internal.example.org"],
|
||||
"excludedDNSDomains": ["internal.example.com"],
|
||||
"permittedIPRanges": ["192.168.1.0/24", "192.168.2.1/32"],
|
||||
"excludedIPRanges": ["192.168.3.0/24", "192.168.4.0/28"],
|
||||
"permittedEmailAddresses": ["root@example.org", "example.org", ".acme.org"],
|
||||
"excludedEmailAddresses": ["root@example.com", "example.com", ".acme.com"],
|
||||
"permittedURIDomains": ["host.example.org", ".acme.org"],
|
||||
"excludedURIDomains": ["host.example.com", ".acme.com"]
|
||||
}
|
||||
}`),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
chain []*x509.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Engine
|
||||
}{
|
||||
{"ok", args{[]*x509.Certificate{ca1.Intermediate, ca1.Root}}, &Engine{
|
||||
hasNameConstraints: false,
|
||||
}},
|
||||
{"ok with constraints", args{[]*x509.Certificate{ca2.Intermediate, ca2.Root}}, &Engine{
|
||||
hasNameConstraints: true,
|
||||
permittedDNSDomains: []string{"internal.example.org"},
|
||||
excludedDNSDomains: []string{"internal.example.com"},
|
||||
permittedIPRanges: []*net.IPNet{
|
||||
{IP: net.ParseIP("192.168.1.0").To4(), Mask: net.IPMask{255, 255, 255, 0}},
|
||||
{IP: net.ParseIP("192.168.2.1").To4(), Mask: net.IPMask{255, 255, 255, 255}},
|
||||
},
|
||||
excludedIPRanges: []*net.IPNet{
|
||||
{IP: net.ParseIP("192.168.3.0").To4(), Mask: net.IPMask{255, 255, 255, 0}},
|
||||
{IP: net.ParseIP("192.168.4.0").To4(), Mask: net.IPMask{255, 255, 255, 240}},
|
||||
},
|
||||
permittedEmailAddresses: []string{"root@example.org", "example.org", ".acme.org"},
|
||||
excludedEmailAddresses: []string{"root@example.com", "example.com", ".acme.com"},
|
||||
permittedURIDomains: []string{"host.example.org", ".acme.org"},
|
||||
excludedURIDomains: []string{"host.example.com", ".acme.com"},
|
||||
}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := New(tt.args.chain...); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("New() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_hasNameConstraints(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fn func(c *x509.Certificate)
|
||||
want bool
|
||||
}{
|
||||
{"no constraints", func(c *x509.Certificate) {}, false},
|
||||
{"permittedDNSDomains", func(c *x509.Certificate) { c.PermittedDNSDomains = []string{"constraint"} }, true},
|
||||
{"excludedDNSDomains", func(c *x509.Certificate) { c.ExcludedDNSDomains = []string{"constraint"} }, true},
|
||||
{"permittedIPRanges", func(c *x509.Certificate) {
|
||||
c.PermittedIPRanges = []*net.IPNet{{IP: net.ParseIP("192.168.3.0").To4(), Mask: net.IPMask{255, 255, 255, 0}}}
|
||||
}, true},
|
||||
{"excludedIPRanges", func(c *x509.Certificate) {
|
||||
c.ExcludedIPRanges = []*net.IPNet{{IP: net.ParseIP("192.168.3.0").To4(), Mask: net.IPMask{255, 255, 255, 0}}}
|
||||
}, true},
|
||||
{"permittedEmailAddresses", func(c *x509.Certificate) { c.PermittedEmailAddresses = []string{"constraint"} }, true},
|
||||
{"excludedEmailAddresses", func(c *x509.Certificate) { c.ExcludedEmailAddresses = []string{"constraint"} }, true},
|
||||
{"permittedURIDomains", func(c *x509.Certificate) { c.PermittedURIDomains = []string{"constraint"} }, true},
|
||||
{"excludedURIDomains", func(c *x509.Certificate) { c.ExcludedURIDomains = []string{"constraint"} }, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cert := &x509.Certificate{}
|
||||
tt.fn(cert)
|
||||
if e := New(cert); e.hasNameConstraints != tt.want {
|
||||
t.Errorf("Engine.hasNameConstraints = %v, want %v", e.hasNameConstraints, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngine_Validate(t *testing.T) {
|
||||
type fields struct {
|
||||
hasNameConstraints bool
|
||||
permittedDNSDomains []string
|
||||
excludedDNSDomains []string
|
||||
permittedIPRanges []*net.IPNet
|
||||
excludedIPRanges []*net.IPNet
|
||||
permittedEmailAddresses []string
|
||||
excludedEmailAddresses []string
|
||||
permittedURIDomains []string
|
||||
excludedURIDomains []string
|
||||
}
|
||||
type args struct {
|
||||
dnsNames []string
|
||||
ipAddresses []net.IP
|
||||
emailAddresses []string
|
||||
uris []*url.URL
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{hasNameConstraints: false}, args{
|
||||
dnsNames: []string{"example.com", "host.example.com"},
|
||||
ipAddresses: []net.IP{{192, 168, 1, 1}, {0x26, 0x00, 0x1f, 0x1c, 0x47, 0x01, 0x9d, 0x00, 0xc3, 0xa7, 0x66, 0x94, 0x87, 0x0f, 0x20, 0x72}},
|
||||
emailAddresses: []string{"root@example.com"},
|
||||
uris: []*url.URL{{Scheme: "https", Host: "example.com", Path: "/uuid/c6d1a755-0c12-431e-9136-b64cb3173ec7"}},
|
||||
}, false},
|
||||
{"ok permitted dns", fields{
|
||||
hasNameConstraints: true,
|
||||
permittedDNSDomains: []string{"example.com"},
|
||||
}, args{dnsNames: []string{"example.com", "www.example.com"}}, false},
|
||||
{"ok not excluded dns", fields{
|
||||
hasNameConstraints: true,
|
||||
excludedDNSDomains: []string{"example.org"},
|
||||
}, args{dnsNames: []string{"example.com", "www.example.com"}}, false},
|
||||
{"ok permitted ip", fields{
|
||||
hasNameConstraints: true,
|
||||
permittedIPRanges: []*net.IPNet{
|
||||
{IP: net.ParseIP("192.168.1.0"), Mask: net.IPMask{255, 255, 255, 0}},
|
||||
{IP: net.ParseIP("192.168.2.1").To4(), Mask: net.IPMask{255, 255, 255, 255}},
|
||||
{IP: net.ParseIP("2600:1700:22f8:2600:e559:bd88:350a:34d6"), Mask: net.IPMask{255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
|
||||
},
|
||||
}, args{ipAddresses: []net.IP{{192, 168, 1, 10}, {192, 168, 2, 1}, {0x26, 0x0, 0x17, 0x00, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc}}}, false},
|
||||
{"ok not excluded ip", fields{
|
||||
hasNameConstraints: true,
|
||||
excludedIPRanges: []*net.IPNet{
|
||||
{IP: net.ParseIP("192.168.1.0"), Mask: net.IPMask{255, 255, 255, 0}},
|
||||
{IP: net.ParseIP("192.168.2.1").To4(), Mask: net.IPMask{255, 255, 255, 255}},
|
||||
},
|
||||
}, args{ipAddresses: []net.IP{{192, 168, 2, 2}, {192, 168, 3, 1}}}, false},
|
||||
{"ok permitted emails", fields{
|
||||
hasNameConstraints: true,
|
||||
permittedEmailAddresses: []string{"root@example.com", "acme.org", ".acme.com"},
|
||||
}, args{emailAddresses: []string{"root@example.com", "name@acme.org", "name@coyote.acme.com", `"(quoted)"@www.acme.com`}}, false},
|
||||
{"ok not excluded emails", fields{
|
||||
hasNameConstraints: true,
|
||||
excludedEmailAddresses: []string{"root@example.com", "acme.org", ".acme.com"},
|
||||
}, args{emailAddresses: []string{"name@example.com", "root@acme.com", "root@other.com"}}, false},
|
||||
{"ok permitted uris", fields{
|
||||
hasNameConstraints: true,
|
||||
permittedURIDomains: []string{"example.com", ".acme.com"},
|
||||
}, args{uris: []*url.URL{{Scheme: "https", Host: "example.com", Path: "/path"}, {Scheme: "https", Host: "www.acme.com", Path: "/path"}}}, false},
|
||||
{"ok not excluded uris", fields{
|
||||
hasNameConstraints: true,
|
||||
excludedURIDomains: []string{"example.com", ".acme.com"},
|
||||
}, args{uris: []*url.URL{{Scheme: "https", Host: "example.org", Path: "/path"}, {Scheme: "https", Host: "acme.com", Path: "/path"}}}, false},
|
||||
{"fail permitted dns", fields{
|
||||
hasNameConstraints: true,
|
||||
permittedDNSDomains: []string{"example.com"},
|
||||
}, args{dnsNames: []string{"www.example.com", "www.example.org"}}, true},
|
||||
{"fail not excluded dns", fields{
|
||||
hasNameConstraints: true,
|
||||
excludedDNSDomains: []string{"example.org"},
|
||||
}, args{dnsNames: []string{"example.com", "www.example.org"}}, true},
|
||||
{"fail permitted ip", fields{
|
||||
hasNameConstraints: true,
|
||||
permittedIPRanges: []*net.IPNet{
|
||||
{IP: net.ParseIP("192.168.1.0").To4(), Mask: net.IPMask{255, 255, 255, 0}},
|
||||
{IP: net.ParseIP("192.168.2.1").To4(), Mask: net.IPMask{255, 255, 255, 255}},
|
||||
},
|
||||
}, args{ipAddresses: []net.IP{{192, 168, 1, 10}, {192, 168, 2, 10}}}, true},
|
||||
{"fail not excluded ip", fields{
|
||||
hasNameConstraints: true,
|
||||
excludedIPRanges: []*net.IPNet{
|
||||
{IP: net.ParseIP("192.168.1.0").To4(), Mask: net.IPMask{255, 255, 255, 0}},
|
||||
{IP: net.ParseIP("192.168.2.1").To4(), Mask: net.IPMask{255, 255, 255, 255}},
|
||||
},
|
||||
}, args{ipAddresses: []net.IP{{192, 168, 2, 2}, {192, 168, 1, 1}}}, true},
|
||||
{"fail permitted emails", fields{
|
||||
hasNameConstraints: true,
|
||||
permittedEmailAddresses: []string{"root@example.com", "acme.org", ".acme.com"},
|
||||
}, args{emailAddresses: []string{"root@example.com", "name@acme.org", "name@acme.com"}}, true},
|
||||
{"fail not excluded emails", fields{
|
||||
hasNameConstraints: true,
|
||||
excludedEmailAddresses: []string{"root@example.com", "acme.org", ".acme.com"},
|
||||
}, args{emailAddresses: []string{"name@example.com", "root@example.com"}}, true},
|
||||
{"fail permitted uris", fields{
|
||||
hasNameConstraints: true,
|
||||
permittedURIDomains: []string{"example.com", ".acme.com"},
|
||||
}, args{uris: []*url.URL{{Scheme: "https", Host: "example.com", Path: "/path"}, {Scheme: "https", Host: "acme.com", Path: "/path"}}}, true},
|
||||
{"fail not excluded uris", fields{
|
||||
hasNameConstraints: true,
|
||||
excludedURIDomains: []string{"example.com", ".acme.com"},
|
||||
}, args{uris: []*url.URL{{Scheme: "https", Host: "www.example.com", Path: "/path"}, {Scheme: "https", Host: "acme.com", Path: "/path"}}}, true},
|
||||
{"fail parse emails", fields{
|
||||
hasNameConstraints: true,
|
||||
permittedEmailAddresses: []string{"example.com"},
|
||||
}, args{emailAddresses: []string{`(notquoted)@example.com`}}, true},
|
||||
{"fail match dns", fields{
|
||||
hasNameConstraints: true,
|
||||
permittedDNSDomains: []string{"example.com"},
|
||||
}, args{dnsNames: []string{`www.example.com.`}}, true},
|
||||
{"fail match email", fields{
|
||||
hasNameConstraints: true,
|
||||
excludedEmailAddresses: []string{`(notquoted)@example.com`},
|
||||
}, args{emailAddresses: []string{`ok@example.com`}}, true},
|
||||
{"fail match uri", fields{
|
||||
hasNameConstraints: true,
|
||||
permittedURIDomains: []string{"example.com"},
|
||||
}, args{uris: []*url.URL{{Scheme: "urn", Opaque: "uuid:36efb1ae-6617-4b23-b799-874a37aaea1c"}}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := &Engine{
|
||||
hasNameConstraints: tt.fields.hasNameConstraints,
|
||||
permittedDNSDomains: tt.fields.permittedDNSDomains,
|
||||
excludedDNSDomains: tt.fields.excludedDNSDomains,
|
||||
permittedIPRanges: tt.fields.permittedIPRanges,
|
||||
excludedIPRanges: tt.fields.excludedIPRanges,
|
||||
permittedEmailAddresses: tt.fields.permittedEmailAddresses,
|
||||
excludedEmailAddresses: tt.fields.excludedEmailAddresses,
|
||||
permittedURIDomains: tt.fields.permittedURIDomains,
|
||||
excludedURIDomains: tt.fields.excludedURIDomains,
|
||||
}
|
||||
if err := e.Validate(tt.args.dnsNames, tt.args.ipAddresses, tt.args.emailAddresses, tt.args.uris); (err != nil) != tt.wantErr {
|
||||
t.Errorf("service.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngine_Validate_nil(t *testing.T) {
|
||||
var e *Engine
|
||||
if err := e.Validate([]string{"www.example.com"}, nil, nil, nil); err != nil {
|
||||
t.Errorf("service.Validate() error = %v, wantErr false", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngine_ValidateCertificate(t *testing.T) {
|
||||
type fields struct {
|
||||
hasNameConstraints bool
|
||||
permittedDNSDomains []string
|
||||
excludedDNSDomains []string
|
||||
permittedIPRanges []*net.IPNet
|
||||
excludedIPRanges []*net.IPNet
|
||||
permittedEmailAddresses []string
|
||||
excludedEmailAddresses []string
|
||||
permittedURIDomains []string
|
||||
excludedURIDomains []string
|
||||
}
|
||||
type args struct {
|
||||
cert *x509.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{hasNameConstraints: false}, args{&x509.Certificate{
|
||||
DNSNames: []string{"example.com"},
|
||||
IPAddresses: []net.IP{{127, 0, 0, 1}},
|
||||
EmailAddresses: []string{"info@example.com"},
|
||||
URIs: []*url.URL{{Scheme: "https", Host: "uuid.example.com", Path: "/dc4c76b5-5262-4551-a881-48094a604d63"}},
|
||||
}}, false},
|
||||
{"ok with constraints", fields{
|
||||
hasNameConstraints: true,
|
||||
permittedDNSDomains: []string{"example.com"},
|
||||
permittedIPRanges: []*net.IPNet{
|
||||
{IP: net.ParseIP("127.0.0.1").To4(), Mask: net.IPMask{255, 255, 255, 255}},
|
||||
{IP: net.ParseIP("10.3.0.0").To4(), Mask: net.IPMask{255, 255, 0, 0}},
|
||||
},
|
||||
permittedEmailAddresses: []string{"example.com"},
|
||||
permittedURIDomains: []string{".example.com"},
|
||||
}, args{&x509.Certificate{
|
||||
DNSNames: []string{"www.example.com"},
|
||||
IPAddresses: []net.IP{{127, 0, 0, 1}, {10, 3, 1, 1}},
|
||||
EmailAddresses: []string{"info@example.com"},
|
||||
URIs: []*url.URL{{Scheme: "https", Host: "uuid.example.com", Path: "/dc4c76b5-5262-4551-a881-48094a604d63"}},
|
||||
}}, false},
|
||||
{"fail", fields{
|
||||
hasNameConstraints: true,
|
||||
permittedURIDomains: []string{".example.com"},
|
||||
}, args{&x509.Certificate{
|
||||
DNSNames: []string{"example.com"},
|
||||
IPAddresses: []net.IP{{127, 0, 0, 1}},
|
||||
EmailAddresses: []string{"info@example.com"},
|
||||
URIs: []*url.URL{{Scheme: "https", Host: "uuid.example.org", Path: "/dc4c76b5-5262-4551-a881-48094a604d63"}},
|
||||
}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := &Engine{
|
||||
hasNameConstraints: tt.fields.hasNameConstraints,
|
||||
permittedDNSDomains: tt.fields.permittedDNSDomains,
|
||||
excludedDNSDomains: tt.fields.excludedDNSDomains,
|
||||
permittedIPRanges: tt.fields.permittedIPRanges,
|
||||
excludedIPRanges: tt.fields.excludedIPRanges,
|
||||
permittedEmailAddresses: tt.fields.permittedEmailAddresses,
|
||||
excludedEmailAddresses: tt.fields.excludedEmailAddresses,
|
||||
permittedURIDomains: tt.fields.permittedURIDomains,
|
||||
excludedURIDomains: tt.fields.excludedURIDomains,
|
||||
}
|
||||
if err := e.ValidateCertificate(tt.args.cert); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Engine.ValidateCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
383
authority/internal/constraints/verify.go
Normal file
383
authority/internal/constraints/verify.go
Normal file
|
@ -0,0 +1,383 @@
|
|||
// Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package constraints
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func checkNameConstraints(nameType, name string, parsedName, permitted, excluded any, match func(name, constraint any) (bool, error)) error {
|
||||
excludedValue := reflect.ValueOf(excluded)
|
||||
for i := 0; i < excludedValue.Len(); i++ {
|
||||
constraint := excludedValue.Index(i).Interface()
|
||||
match, err := match(parsedName, constraint)
|
||||
if err != nil {
|
||||
return ConstraintError{
|
||||
Type: nameType,
|
||||
Name: name,
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
if match {
|
||||
return ConstraintError{
|
||||
Type: nameType,
|
||||
Name: name,
|
||||
Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
ok = true
|
||||
)
|
||||
|
||||
permittedValue := reflect.ValueOf(permitted)
|
||||
for i := 0; i < permittedValue.Len(); i++ {
|
||||
constraint := permittedValue.Index(i).Interface()
|
||||
if ok, err = match(parsedName, constraint); err != nil {
|
||||
return ConstraintError{
|
||||
Type: nameType,
|
||||
Name: name,
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return ConstraintError{
|
||||
Type: nameType,
|
||||
Name: name,
|
||||
Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func matchDomainConstraint(domain, constraint string) (bool, error) {
|
||||
// The meaning of zero length constraints is not specified, but this
|
||||
// code follows NSS and accepts them as matching everything.
|
||||
if constraint == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
domainLabels, ok := domainToReverseLabels(domain)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("internal error: cannot parse domain %q", domain)
|
||||
}
|
||||
|
||||
// RFC 5280 says that a leading period in a domain name means that at least
|
||||
// one label must be prepended, but only for URI and email constraints, not
|
||||
// DNS constraints. The code also supports that behavior for DNS
|
||||
// constraints.
|
||||
|
||||
mustHaveSubdomains := false
|
||||
if constraint[0] == '.' {
|
||||
mustHaveSubdomains = true
|
||||
constraint = constraint[1:]
|
||||
}
|
||||
|
||||
constraintLabels, ok := domainToReverseLabels(constraint)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("internal error: cannot parse domain %q", constraint)
|
||||
}
|
||||
|
||||
if len(domainLabels) < len(constraintLabels) ||
|
||||
(mustHaveSubdomains && len(domainLabels) == len(constraintLabels)) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for i, constraintLabel := range constraintLabels {
|
||||
if !strings.EqualFold(constraintLabel, domainLabels[i]) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func normalizeIP(ip net.IP) net.IP {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return ip4
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) {
|
||||
ip = normalizeIP(ip)
|
||||
constraintIP := normalizeIP(constraint.IP)
|
||||
if len(ip) != len(constraintIP) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for i := range ip {
|
||||
if mask := constraint.Mask[i]; ip[i]&mask != constraintIP[i]&mask {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) {
|
||||
// If the constraint contains an @, then it specifies an exact mailbox
|
||||
// name.
|
||||
if strings.Contains(constraint, "@") {
|
||||
constraintMailbox, ok := parseRFC2821Mailbox(constraint)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("internal error: cannot parse constraint %q", constraint)
|
||||
}
|
||||
return mailbox.local == constraintMailbox.local && strings.EqualFold(mailbox.domain, constraintMailbox.domain), nil
|
||||
}
|
||||
|
||||
// Otherwise the constraint is like a DNS constraint of the domain part
|
||||
// of the mailbox.
|
||||
return matchDomainConstraint(mailbox.domain, constraint)
|
||||
}
|
||||
|
||||
func matchURIConstraint(uri *url.URL, constraint string) (bool, error) {
|
||||
// From RFC 5280, Section 4.2.1.10:
|
||||
// “a uniformResourceIdentifier that does not include an authority
|
||||
// component with a host name specified as a fully qualified domain
|
||||
// name (e.g., if the URI either does not include an authority
|
||||
// component or includes an authority component in which the host name
|
||||
// is specified as an IP address), then the application MUST reject the
|
||||
// certificate.”
|
||||
|
||||
host := uri.Host
|
||||
if host == "" {
|
||||
return false, fmt.Errorf("URI with empty host (%q) cannot be matched against constraints", uri.String())
|
||||
}
|
||||
|
||||
if strings.Contains(host, ":") && !strings.HasSuffix(host, "]") {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(uri.Host)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") ||
|
||||
net.ParseIP(host) != nil {
|
||||
return false, fmt.Errorf("URI with IP (%q) cannot be matched against constraints", uri.String())
|
||||
}
|
||||
|
||||
return matchDomainConstraint(host, constraint)
|
||||
}
|
||||
|
||||
// domainToReverseLabels converts a textual domain name like foo.example.com to
|
||||
// the list of labels in reverse order, e.g. ["com", "example", "foo"].
|
||||
func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {
|
||||
for len(domain) > 0 {
|
||||
if i := strings.LastIndexByte(domain, '.'); i == -1 {
|
||||
reverseLabels = append(reverseLabels, domain)
|
||||
domain = ""
|
||||
} else {
|
||||
reverseLabels = append(reverseLabels, domain[i+1:])
|
||||
domain = domain[:i]
|
||||
}
|
||||
}
|
||||
|
||||
if len(reverseLabels) > 0 && reverseLabels[0] == "" {
|
||||
// An empty label at the end indicates an absolute value.
|
||||
return nil, false
|
||||
}
|
||||
|
||||
for _, label := range reverseLabels {
|
||||
if label == "" {
|
||||
// Empty labels are otherwise invalid.
|
||||
return nil, false
|
||||
}
|
||||
|
||||
for _, c := range label {
|
||||
if c < 33 || c > 126 {
|
||||
// Invalid character.
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reverseLabels, true
|
||||
}
|
||||
|
||||
// rfc2821Mailbox represents a “mailbox” (which is an email address to most
|
||||
// people) by breaking it into the “local” (i.e. before the '@') and “domain”
|
||||
// parts.
|
||||
type rfc2821Mailbox struct {
|
||||
local, domain string
|
||||
}
|
||||
|
||||
// parseRFC2821Mailbox parses an email address into local and domain parts,
|
||||
// based on the ABNF for a “Mailbox” from RFC 2821. According to RFC 5280,
|
||||
// Section 4.2.1.6 that's correct for an rfc822Name from a certificate: “The
|
||||
// format of an rfc822Name is a "Mailbox" as defined in RFC 2821, Section 4.1.2”.
|
||||
func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {
|
||||
if in == "" {
|
||||
return mailbox, false
|
||||
}
|
||||
|
||||
localPartBytes := make([]byte, 0, len(in)/2)
|
||||
|
||||
if in[0] == '"' {
|
||||
// Quoted-string = DQUOTE *qcontent DQUOTE
|
||||
// non-whitespace-control = %d1-8 / %d11 / %d12 / %d14-31 / %d127
|
||||
// qcontent = qtext / quoted-pair
|
||||
// qtext = non-whitespace-control /
|
||||
// %d33 / %d35-91 / %d93-126
|
||||
// quoted-pair = ("\" text) / obs-qp
|
||||
// text = %d1-9 / %d11 / %d12 / %d14-127 / obs-text
|
||||
//
|
||||
// (Names beginning with “obs-” are the obsolete syntax from RFC 2822,
|
||||
// Section 4. Since it has been 16 years, we no longer accept that.)
|
||||
in = in[1:]
|
||||
QuotedString:
|
||||
for {
|
||||
if in == "" {
|
||||
return mailbox, false
|
||||
}
|
||||
c := in[0]
|
||||
in = in[1:]
|
||||
|
||||
switch {
|
||||
case c == '"':
|
||||
break QuotedString
|
||||
|
||||
case c == '\\':
|
||||
// quoted-pair
|
||||
if in == "" {
|
||||
return mailbox, false
|
||||
}
|
||||
if in[0] == 11 ||
|
||||
in[0] == 12 ||
|
||||
(1 <= in[0] && in[0] <= 9) ||
|
||||
(14 <= in[0] && in[0] <= 127) {
|
||||
localPartBytes = append(localPartBytes, in[0])
|
||||
in = in[1:]
|
||||
} else {
|
||||
return mailbox, false
|
||||
}
|
||||
|
||||
case c == 11 ||
|
||||
c == 12 ||
|
||||
// Space (char 32) is not allowed based on the
|
||||
// BNF, but RFC 3696 gives an example that
|
||||
// assumes that it is. Several “verified”
|
||||
// errata continue to argue about this point.
|
||||
// We choose to accept it.
|
||||
c == 32 ||
|
||||
c == 33 ||
|
||||
c == 127 ||
|
||||
(1 <= c && c <= 8) ||
|
||||
(14 <= c && c <= 31) ||
|
||||
(35 <= c && c <= 91) ||
|
||||
(93 <= c && c <= 126):
|
||||
// qtext
|
||||
localPartBytes = append(localPartBytes, c)
|
||||
|
||||
default:
|
||||
return mailbox, false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Atom ("." Atom)*
|
||||
NextChar:
|
||||
for len(in) > 0 {
|
||||
// atext from RFC 2822, Section 3.2.4
|
||||
c := in[0]
|
||||
|
||||
switch {
|
||||
case c == '\\':
|
||||
// Examples given in RFC 3696 suggest that
|
||||
// escaped characters can appear outside of a
|
||||
// quoted string. Several “verified” errata
|
||||
// continue to argue the point. We choose to
|
||||
// accept it.
|
||||
in = in[1:]
|
||||
if in == "" {
|
||||
return mailbox, false
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case ('0' <= c && c <= '9') ||
|
||||
('a' <= c && c <= 'z') ||
|
||||
('A' <= c && c <= 'Z') ||
|
||||
c == '!' || c == '#' || c == '$' || c == '%' ||
|
||||
c == '&' || c == '\'' || c == '*' || c == '+' ||
|
||||
c == '-' || c == '/' || c == '=' || c == '?' ||
|
||||
c == '^' || c == '_' || c == '`' || c == '{' ||
|
||||
c == '|' || c == '}' || c == '~' || c == '.':
|
||||
localPartBytes = append(localPartBytes, in[0])
|
||||
in = in[1:]
|
||||
|
||||
default:
|
||||
break NextChar
|
||||
}
|
||||
}
|
||||
|
||||
if len(localPartBytes) == 0 {
|
||||
return mailbox, false
|
||||
}
|
||||
|
||||
// From RFC 3696, Section 3:
|
||||
// “period (".") may also appear, but may not be used to start
|
||||
// or end the local part, nor may two or more consecutive
|
||||
// periods appear.”
|
||||
twoDots := []byte{'.', '.'}
|
||||
if localPartBytes[0] == '.' ||
|
||||
localPartBytes[len(localPartBytes)-1] == '.' ||
|
||||
bytes.Contains(localPartBytes, twoDots) {
|
||||
return mailbox, false
|
||||
}
|
||||
}
|
||||
|
||||
if in == "" || in[0] != '@' {
|
||||
return mailbox, false
|
||||
}
|
||||
in = in[1:]
|
||||
|
||||
// The RFC species a format for domains, but that's known to be
|
||||
// violated in practice so we accept that anything after an '@' is the
|
||||
// domain part.
|
||||
if _, ok := domainToReverseLabels(in); !ok {
|
||||
return mailbox, false
|
||||
}
|
||||
|
||||
mailbox.local = string(localPartBytes)
|
||||
mailbox.domain = in
|
||||
return mailbox, true
|
||||
}
|
|
@ -265,8 +265,20 @@ func (c *linkedCaClient) GetCertificateData(serial string) (*db.CertificateData,
|
|||
ID: p.Id, Name: p.Name, Type: p.Type.String(),
|
||||
}
|
||||
}
|
||||
|
||||
var raInfo *provisioner.RAInfo
|
||||
if p := resp.RaProvisioner; p != nil && p.Provisioner != nil {
|
||||
raInfo = &provisioner.RAInfo{
|
||||
AuthorityID: p.AuthorityId,
|
||||
ProvisionerID: p.Provisioner.Id,
|
||||
ProvisionerType: p.Provisioner.Type.String(),
|
||||
ProvisionerName: p.Provisioner.Name,
|
||||
}
|
||||
}
|
||||
|
||||
return &db.CertificateData{
|
||||
Provisioner: pd,
|
||||
RaInfo: raInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -278,6 +290,7 @@ func (c *linkedCaClient) StoreCertificateChain(p provisioner.Interface, fullchai
|
|||
PemCertificate: serializeCertificateChain(fullchain[0]),
|
||||
PemCertificateChain: serializeCertificateChain(fullchain[1:]...),
|
||||
Provisioner: createProvisionerIdentity(p),
|
||||
AttestationData: createAttestationData(p),
|
||||
RaProvisioner: raProvisioner,
|
||||
EndpointId: endpointID,
|
||||
})
|
||||
|
@ -368,19 +381,19 @@ func (c *linkedCaClient) IsSSHRevoked(serial string) (bool, error) {
|
|||
return resp.Status != linkedca.RevocationStatus_ACTIVE, nil
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||
func (c *linkedCaClient) CreateAuthorityPolicy(_ context.Context, _ *linkedca.Policy) error {
|
||||
return errors.New("not implemented yet")
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
|
||||
func (c *linkedCaClient) GetAuthorityPolicy(context.Context) (*linkedca.Policy, error) {
|
||||
return nil, errors.New("not implemented yet")
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||
func (c *linkedCaClient) UpdateAuthorityPolicy(_ context.Context, _ *linkedca.Policy) error {
|
||||
return errors.New("not implemented yet")
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) DeleteAuthorityPolicy(ctx context.Context) error {
|
||||
func (c *linkedCaClient) DeleteAuthorityPolicy(context.Context) error {
|
||||
return errors.New("not implemented yet")
|
||||
}
|
||||
|
||||
|
@ -395,26 +408,34 @@ func createProvisionerIdentity(p provisioner.Interface) *linkedca.ProvisionerIde
|
|||
}
|
||||
}
|
||||
|
||||
type raProvisioner interface {
|
||||
RAInfo() *provisioner.RAInfo
|
||||
}
|
||||
|
||||
func createRegistrationAuthorityProvisioner(p provisioner.Interface) (*linkedca.RegistrationAuthorityProvisioner, string) {
|
||||
if rap, ok := p.(raProvisioner); ok {
|
||||
info := rap.RAInfo()
|
||||
typ := linkedca.Provisioner_Type_value[strings.ToUpper(info.ProvisionerType)]
|
||||
return &linkedca.RegistrationAuthorityProvisioner{
|
||||
AuthorityId: info.AuthorityID,
|
||||
Provisioner: &linkedca.ProvisionerIdentity{
|
||||
Id: info.ProvisionerID,
|
||||
Type: linkedca.Provisioner_Type(typ),
|
||||
Name: info.ProvisionerName,
|
||||
},
|
||||
}, info.EndpointID
|
||||
if info := rap.RAInfo(); info != nil {
|
||||
typ := linkedca.Provisioner_Type_value[strings.ToUpper(info.ProvisionerType)]
|
||||
return &linkedca.RegistrationAuthorityProvisioner{
|
||||
AuthorityId: info.AuthorityID,
|
||||
Provisioner: &linkedca.ProvisionerIdentity{
|
||||
Id: info.ProvisionerID,
|
||||
Type: linkedca.Provisioner_Type(typ),
|
||||
Name: info.ProvisionerName,
|
||||
},
|
||||
}, info.EndpointID
|
||||
}
|
||||
}
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
func createAttestationData(p provisioner.Interface) *linkedca.AttestationData {
|
||||
if ap, ok := p.(attProvisioner); ok {
|
||||
if data := ap.AttestationData(); data != nil {
|
||||
return &linkedca.AttestationData{
|
||||
PermanentIdentifier: data.PermanentIdentifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func serializeCertificate(crt *x509.Certificate) string {
|
||||
if crt == nil {
|
||||
return ""
|
||||
|
@ -461,7 +482,7 @@ func getRootCertificate(endpoint, fingerprint string) (*x509.Certificate, error)
|
|||
defer cancel()
|
||||
|
||||
conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
||||
// nolint:gosec // used in bootstrap protocol
|
||||
//nolint:gosec // used in bootstrap protocol
|
||||
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
|
||||
})))
|
||||
if err != nil {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
@ -85,6 +86,22 @@ func WithDatabase(d db.AuthDB) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithQuietInit disables log output when the authority is initialized.
|
||||
func WithQuietInit() Option {
|
||||
return func(a *Authority) error {
|
||||
a.quietInit = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithWebhookClient sets the http.Client to be used for outbound requests.
|
||||
func WithWebhookClient(c *http.Client) Option {
|
||||
return func(a *Authority) error {
|
||||
a.webhookClient = c
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithGetIdentityFunc sets a custom function to retrieve the identity from
|
||||
// an external resource.
|
||||
func WithGetIdentityFunc(fn func(ctx context.Context, p provisioner.Interface, email string) (*provisioner.Identity, error)) Option {
|
||||
|
@ -151,16 +168,23 @@ func WithKeyManager(k kms.KeyManager) Option {
|
|||
|
||||
// WithX509Signer defines the signer used to sign X509 certificates.
|
||||
func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option {
|
||||
return WithX509SignerChain([]*x509.Certificate{crt}, s)
|
||||
}
|
||||
|
||||
// WithX509SignerChain defines the signer used to sign X509 certificates. This
|
||||
// option is similar to WithX509Signer but it supports a chain of intermediates.
|
||||
func WithX509SignerChain(issuerChain []*x509.Certificate, s crypto.Signer) Option {
|
||||
return func(a *Authority) error {
|
||||
srv, err := cas.New(context.Background(), casapi.Options{
|
||||
Type: casapi.SoftCAS,
|
||||
Signer: s,
|
||||
CertificateChain: []*x509.Certificate{crt},
|
||||
CertificateChain: issuerChain,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.x509CAService = srv
|
||||
a.intermediateX509Certs = append(a.intermediateX509Certs, issuerChain...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -233,6 +257,25 @@ func WithX509FederatedCerts(certs ...*x509.Certificate) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithX509IntermediateCerts is an option that allows to define the list of
|
||||
// intermediate certificates that the CA will be using. This option will replace
|
||||
// any intermediate certificate defined before.
|
||||
//
|
||||
// Note that these certificates will not be bundled with the certificates signed
|
||||
// by the CA, because the CAS service will take care of that. They should match,
|
||||
// but that's not guaranteed. These certificates will be mainly used for name
|
||||
// constraint validation before a certificate is issued.
|
||||
//
|
||||
// This option should only be used on specific configurations, for example when
|
||||
// WithX509SignerFunc is used, as we don't know the list of intermediates in
|
||||
// advance.
|
||||
func WithX509IntermediateCerts(intermediateCerts ...*x509.Certificate) Option {
|
||||
return func(a *Authority) error {
|
||||
a.intermediateX509Certs = intermediateCerts
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithX509RootBundle is an option that allows to define the list of root
|
||||
// certificates. This option will replace any root certificate defined before.
|
||||
func WithX509RootBundle(pemCerts []byte) Option {
|
||||
|
|
|
@ -119,7 +119,6 @@ func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error {
|
|||
}
|
||||
|
||||
func (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *linkedca.Admin, p *linkedca.Policy) error {
|
||||
|
||||
// no policy and thus nothing to evaluate; return early
|
||||
if p == nil {
|
||||
return nil
|
||||
|
@ -138,7 +137,6 @@ func (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *link
|
|||
}
|
||||
|
||||
func (a *Authority) checkProvisionerPolicy(ctx context.Context, provName string, p *linkedca.Policy) error {
|
||||
|
||||
// no policy and thus nothing to evaluate; return early
|
||||
if p == nil {
|
||||
return nil
|
||||
|
@ -156,8 +154,7 @@ func (a *Authority) checkProvisionerPolicy(ctx context.Context, provName string,
|
|||
|
||||
// checkPolicy checks if a new or updated policy configuration results in the user
|
||||
// locking themselves or other admins out of the CA.
|
||||
func (a *Authority) checkPolicy(ctx context.Context, currentAdmin *linkedca.Admin, otherAdmins []*linkedca.Admin, p *linkedca.Policy) error {
|
||||
|
||||
func (a *Authority) checkPolicy(_ context.Context, currentAdmin *linkedca.Admin, otherAdmins []*linkedca.Admin, p *linkedca.Policy) error {
|
||||
// convert the policy; return early if nil
|
||||
policyOptions := authPolicy.LinkedToCertificates(p)
|
||||
if policyOptions == nil {
|
||||
|
@ -216,7 +213,6 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error {
|
|||
)
|
||||
|
||||
if a.config.AuthorityConfig.EnableAdmin {
|
||||
|
||||
// temporarily disable policy loading when LinkedCA is in use
|
||||
if _, ok := a.adminDB.(*linkedCaClient); ok {
|
||||
return nil
|
||||
|
@ -252,7 +248,7 @@ func isAllowed(engine authPolicy.X509Policy, sans []string) error {
|
|||
if isNamePolicyError && policyErr.Reason == policy.NotAllowed {
|
||||
return &PolicyError{
|
||||
Typ: AdminLockOut,
|
||||
Err: fmt.Errorf("the provided policy would lock out %s from the CA. Please update your policy to include %s as an allowed name", sans, sans),
|
||||
Err: fmt.Errorf("the provided policy would lock out %s from the CA. Please create an x509 policy to include %s as an allowed DNS name", sans, sans),
|
||||
}
|
||||
}
|
||||
return &PolicyError{
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue