From faac9b7f3f10c7507986437d6ed564c004a97d75 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 8 Jul 2024 09:59:12 +0800 Subject: [PATCH] feat: Timestamp (#207) Signed-off-by: Patrick Zheng --- go.mod | 1 + go.sum | 2 + internal/oid/oid.go | 31 ++ .../timestamp/testdata/TimeStampToken.p7s | Bin 0 -> 6595 bytes .../TimeStampTokenWithInvalidSignature.p7s | Bin 0 -> 6595 bytes .../TimeStampTokenWithInvalidTSTInfo.p7s | Bin 0 -> 6578 bytes internal/timestamp/testdata/tsaRootCert.crt | Bin 0 -> 1415 bytes internal/timestamp/timestamp.go | 65 ++++ internal/timestamp/timestamp_test.go | 200 ++++++++++++ revocation/ocsp/ocsp.go | 32 +- revocation/ocsp/ocsp_test.go | 33 ++ revocation/revocation.go | 28 +- revocation/revocation_test.go | 241 ++++++++++++++ signature/cose/conformance_test.go | 6 +- signature/cose/envelope.go | 43 ++- signature/cose/envelope_test.go | 108 +++++++ signature/errors.go | 25 ++ signature/errors_test.go | 31 ++ signature/internal/base/envelope.go | 3 +- signature/internal/base/envelope_test.go | 80 ++++- signature/jws/envelope.go | 39 +++ signature/jws/envelope_test.go | 88 ++++- signature/jws/types.go | 2 +- signature/types.go | 38 +++ signature/types_test.go | 53 +++ testhelper/certificatetest.go | 52 ++- x509/cert_validations.go | 306 ------------------ x509/codesigning_cert_validations.go | 173 ++++++++++ ...o => codesigning_cert_validations_test.go} | 277 ++++++---------- x509/helper.go | 127 ++++++++ x509/testdata/timestamp_intermediate.crt | Bin 0 -> 1714 bytes x509/testdata/timestamp_leaf.crt | Bin 0 -> 1734 bytes x509/testdata/timestamp_root.crt | Bin 0 -> 1428 bytes x509/timestamp_cert_validations.go | 161 +++++++++ x509/timestamp_cert_validations_test.go | 217 +++++++++++++ 35 files changed, 1936 insertions(+), 526 deletions(-) create mode 100644 internal/oid/oid.go create mode 100644 internal/timestamp/testdata/TimeStampToken.p7s create mode 100644 internal/timestamp/testdata/TimeStampTokenWithInvalidSignature.p7s create mode 100644 internal/timestamp/testdata/TimeStampTokenWithInvalidTSTInfo.p7s create mode 100644 internal/timestamp/testdata/tsaRootCert.crt create mode 100644 internal/timestamp/timestamp.go create mode 100644 internal/timestamp/timestamp_test.go create mode 100644 signature/types_test.go delete mode 100644 x509/cert_validations.go create mode 100644 x509/codesigning_cert_validations.go rename x509/{cert_validations_test.go => codesigning_cert_validations_test.go} (84%) create mode 100644 x509/helper.go create mode 100644 x509/testdata/timestamp_intermediate.crt create mode 100644 x509/testdata/timestamp_leaf.crt create mode 100644 x509/testdata/timestamp_root.crt create mode 100644 x509/timestamp_cert_validations.go create mode 100644 x509/timestamp_cert_validations_test.go diff --git a/go.mod b/go.mod index b7bec727..9e27e3c8 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 require ( github.com/fxamacker/cbor/v2 v2.7.0 github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/notaryproject/tspclient-go v0.0.0-20240702050734-d91848411058 github.com/veraison/go-cose v1.1.0 golang.org/x/crypto v0.24.0 ) diff --git a/go.sum b/go.sum index 04011b06..b3cfbd4a 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/notaryproject/tspclient-go v0.0.0-20240702050734-d91848411058 h1:FlGmQAwbf78rw12fXT4+9EkmD9+ZWuqH08v0fE3sqHc= +github.com/notaryproject/tspclient-go v0.0.0-20240702050734-d91848411058/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs= github.com/veraison/go-cose v1.1.0 h1:AalPS4VGiKavpAzIlBjrn7bhqXiXi4jbMYY/2+UC+4o= github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= diff --git a/internal/oid/oid.go b/internal/oid/oid.go new file mode 100644 index 00000000..18ccb937 --- /dev/null +++ b/internal/oid/oid.go @@ -0,0 +1,31 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oid + +import "encoding/asn1" + +// KeyUsage (id-ce-keyUsage) is defined in RFC 5280 +// +// Reference: https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.3 +var KeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15} + +// ExtKeyUsage (id-ce-extKeyUsage) is defined in RFC 5280 +// +// Reference: https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.12 +var ExtKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37} + +// Timestamping (id-kp-timeStamping) is defined in RFC 3161 2.3 +// +// Reference: https://datatracker.ietf.org/doc/html/rfc3161#section-2.3 +var Timestamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8} diff --git a/internal/timestamp/testdata/TimeStampToken.p7s b/internal/timestamp/testdata/TimeStampToken.p7s new file mode 100644 index 0000000000000000000000000000000000000000..c036aac23cb9bcb4b30960ba940c41a1bdda1805 GIT binary patch literal 6595 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 F{{ssj24?^O literal 0 HcmV?d00001 diff --git a/internal/timestamp/testdata/TimeStampTokenWithInvalidSignature.p7s b/internal/timestamp/testdata/TimeStampTokenWithInvalidSignature.p7s new file mode 100644 index 0000000000000000000000000000000000000000..5da09e0b8962084ffe2be77916440b4eb2a22676 GIT binary patch literal 6595 zcmc(jc|6qX_rPby82bp5Ez6KS%V%cnOIebADP&8GeWx*Yg;DnGODarqQ$h@pi%6D| zED=fB(qa!G`)}0ke%*Wfy}rxudtbl#V`iS^Jm-1N=Q-znJ_jIhG{R`aqP3&f7{F8z zB7vhEAaImGz!WG(fB{Bh7p)C;qJToclprAT4UF!$nKTE$P-0{!H1a7B*#-fFVKhfz zU?`Y~hEM>gKgmInQ9@sQb5*%F8jGSJn_MHr#R%;}Rn=zZh9r^K9&IPvAqYztd_G25 z$VDszv}8cczz4@(fwMEB0W=CwK*^z`$=`q#Kta|-G%qv*Oi*+H4UEDJ3S)t&Yoa&+ zw!H=|{Q)gcAA78)iJOZz(mO!H4~r57g!hIh=#k%t{Pu=SP1KOiJ~*VBV*u9M!O0OR zfz*;F5)L{71eh5>fT|G5njnw^V9*IS<6F=>L%yH5Ul^$6?;Y$n1Ayts2C@G6Hdy|; z4HO{tZ5u2i`*vvR=H=vv$9nm?dAlGr)X2Lq(N@FAOMlxAz{AEsKD`WD4p5MhL1V1g z7%*r+7A^m+iMIM%Yyc3z1^W+4e(Dqia59M89}gjbBSE8p+m+C42Jf4uRSLF_mi|!w z9YZ&tPi%PnFXvCP!%rZ2T_t~g?=&!Zow$;yIO&Mv{nQw6m&_tYlhnrN=s*hI_jbU->g zkLR7cp-*|3u`ioG5jv+$CMPx<&_$x8NT=r9w zUj6z2ZBVDgiKIDmx?{na#zhq6m?(6OYTlQsv}_i7R>*$rNeWtKX_BIg zO;Mu8zGE!as)U>RW`+|r`mNgDvgKzy1I4()$IH^^#c|L>Xtor+!6OlZl)UdTUp_Ut ziUqR~7a$a15cnnme2x4>vH>{xPH{7W!Mji>1%xUBVBKpA?zQOwS{OCiB`GPXD5wGM zy?!{96^bak;2Rl)M2MCy1%0&y4RziUfFuF>d&5YmCZGYR{qD$M9zYsE|5yY9W)*V9 z<9+=UrKAD_10`L)`PrT`Njms=N#UG)ef-?;KDc11Z+^+m0)+tNfRXY;9t?&?Mgfr# zfWnU+030o`w<38#9|u3*KdcFY$2t8~ z*ue>h|MLQp4mdo(yLZ;ZPjs|{H<$&}2tDc|e@Z9e}q)Nu%-;~9|+Gl`H`{gUu@o98tx0$1yDz^=4b)0)`6a`D0QM7DXGh(*qck6RU^ z+-Rr>qwK{&niCS$7n)QL@^c1bU9zj0lY5Mu#U)L}EMO%$hTHz7rb4u?yJ^B-m1{Y? zfJhXYBA}*F6ggWoIDxwL7|+Fiev0^V(mHK^Qm*v`3a~zhL*OY zvPYZ;n$%vB4(ssuJ+zIJO|%+lH7-5|H#DWG-eE``I`=AHR#`eJE~;j7*;%(sE+xSx zWcujMcT`j=861ebjS3gNnQBrEwNF}>rj7nQ+|nw+*$=BJs~g?rpuyn{Q=~bekb-aX zqdTkK`&^3OQ$V-~0y&v>8C+#^sr~C?HUGDciA&2@=BH|6Xo?zWSpYeEUdxZ7xIj1m zpp^3GT#9Gh4@BeEKqPdwWl)~s*GlK@dq=k7s{IQxXywva$nq=46w8WkSE=_6T|ddZ zv=lOMWHwJK9(Hk#_rR^15~L?8X@;oL$+h+_{X0XLRE9MZK2Il{i7>^$$}d z)v<(^qr0HUtT3=y`U88%Pq65_X1YF@*5%h0l$YOvSCq~8F71TM1h{ZBI8W4UWx*ZLiOy^^?J3Nd}ABcd#&kXSvc;N46zZZNjwh`|k@qnJW4y zB)~(yg-|*cwtciX<>Q0@PKi^pC>DTeZw4(3?LTJx=%m=+xrY%H`6{D<@1H%4NTbML z`K|f)GK>J>-+#JqIDs&daRQ-*?778f$OLeBW9-oQRea?=|0vr#TBq8viVk54hl9QF zTc}AqGe_1&m$1Ec8CAUSy!p)*nYl{o_L9f+GZD^Sy&}4g#OrE(mvCi?YdKan0cDNH zZ-=P~BqjgaYP2h>cDG1kJIGKYb|;;KZD=0VT_3OH?cNgmc|Lpf%`!VtFNX^rQzSGV z`;_=TXCm**b^T*D*0a?MFPyqH=}c8BOiic~a=agy=EUDdr710ioZo_5j7GYTmwWO! z5yblW+B0_4vy>L;D#F80T!F`^ggDLgp1vd5uxX)k|I&s@j)66P4yRYZK#=30@3}i| z^=N_dWlrs}FGezp*BSfdSWXQIO3!62e07IK0G%q4xf&*Xf;jZknS4a2KKWEfNDUqnwn{8T!Fx=avPj|CJi^BeQMq z)YiHReQLeDes!47A@-ND(~f0b+bfhGCY*cVlzb%>Ot8Zolbe*%$ET^wa&=so5(w{0 zt)h#Zw=oppksQJGbKX4#;tPa3V&3;7pW^Gr>RN3>*zG?}cKPJ)BVkF!B%i!t6uYPoww# z@LuLgc{815qTu7iAlE?q=kO;kCPd)q4~US7wC59&KiXs-@$cgi`p>Z*M|MA)EFk+G z4^n^xaD*sI6pj}B-|?^~B7u`7bH@|V+LJ>a;){l?)#RK9JSbTp+PMLP_yLK~|GhpLVsHP7U=wShv0WS*?0 z=mNG8wML`y`n%8Pn+_+oUE0w*D6r0ce3rMlu%w67J==WRiMt_ub%gMwCLh~(|Ah9H zY=}z;Yo_k7eOpf@q*Gd3)9mf+ly3f6#2oIxxd7i6j9JUma^@MS?DyVW&XMP^Xufmf zLV@=|q3NpiaIudcwH@80UR=F_TU`^-!&{xlMaxt|%y|>8Z`GCPk8F&X=tpfcS5)hm z8Tsdy_~<^7bg%7*eas9PlBoUi&)yiw>6$vPmnGiG>cRee(o!y(MtnkZfb`s(;Q!0( z%RDvKQn8XM=NFe!0m|!Lp<%?D9VaXv zJfZyl8qd4pi-fqNO}Z<5JN5_WxPh(J&S`DoumpegaF1upzj$QwLM^0e@0o={%Pg<5D>K)gbzdLp@- zB|g!$UG_?>J~wxYZ|CT0e!c={S;-5j5M;%dl49-&2}JHjmOFQwa>w!zSEGqRp_XM> zW{ZZ}y5NNPfeVrA3>ih|m+sT7yt2co8fPN=HI6^rAZC6&y8BxO# z3BS-1U+<{T$ED#qsG=O-N^*XgaA;H18=QDfU=p)8)6z&9c0wiPy`md+w>K+EZ-cIK1Ez3E8Ql~`vQ z_X?PzQ;~0Mq)VZjAOyhnFKlJjBANPmfxsHPeyP0UoTfQkmFcQF>inGuRs*!*Ry6!l}A3 z@!A`fDF&Yhl5bqRpC&Nfzlewjarj0Br)H=2a|$=g@t~<*Asm!N^zZ$ki4d74{C}be z3}F17CX@gLnI`*C@>5dmM@jN$z3|(TngV48#v2Y-6GkT99;*L8q6G6Fpak>xq67>E z>6Dypjas&Ph7~i&);%biKZ!^wrde+Z$(Lb|r^Ouf!o~0#svYy9&0{Ycp0M4@>=nO) zP)d6AS=KUm`Eq18ewDp&a@@6hA>7GQeX7Gup}wzGX#Sl1#vmUBV~ydEwVk_0)p?p8 zJM7!yInY@{G|w2xGf$b+Tz91}CBWUSl=IE)xYrg&FPan3x?QH5@>5DwbpEtV!*pSS z!iyzB-ApDQ-n30%Wzb)`-bTISP@HLYy@>-o5l}XKV4_^XcK3sf_zHUuLV{NnU|_-YqX~8kfm7G8Y1SML|jpQr7}4DFlPjf^b|m# zuq9`wTHiw{9TY_VfcY#;sQ*wF*{l8h^DO<}45YyR$VsmK>~H>;we7jbzC7Z4m1i$4 z2b0rs@-65T3`hxv1@W`4G(}Zk>=4P@rVL)PUI+@yarQy>LTO-)pas{c+9`Du*UXXS6D0J3rQREL|4i+7&TFRW#mYUK2DmfHTNvuV$Pa1X_J@~ z@T|>n*nDH+>T>#M)PrsrzQ<2pce-9Tn-q7Z#7}*;Igyz8HpD`NQ;K`!IY>?|dG5H^ zpovKKg=H5g&%wd?tg5~dec?j=LwW|#0-?Ig?L+^#jWM+W$I&PQr&uY(uk*uZKBE`sDQOHE zL|);#`s|IJcxKYFqfkSY_dB#Kj@hCJ6b58B(;?S#0iVD1_4DTi+|=dhe}#&ujL{-}vL>CD(X=l!s}cD1In1Z1fo;b3G)m79(n+RE)Y` zKONEMDWoiVFI^`nklQK-h%Dcy49y8JKE8?s^#to8l5(KL!z; z99@R5axg|ZWv_-;cebqqet>PE&HsZ1_m#k*1aQR5U{#%*avPV>c@Mj5l3$0iIT`OB zwYpK!9jbS;#i`Y1_j)Tdp1z53cVSy6kcmBY`Ruh4Z}g#@;#_|}gHr!w%8CL{Y-%k` zi8XI9q^h{k%K8j3{g_xUwe{g+tktPZ$t9Rx{$K9eb#8m4@-#a1w<~&Yf>x8pO-n@K zY1OI-uX8kmH=1kmRhC~BEZ7bwSF+2qm$%_cG*7``-7kAY(>itN2J*RFR)c`MZDU!z z9dzNRS|{Ry8!{a>PE8owGI<`+ACZY&sNkUjQShEUz+AdlnReB(_Ja6*ZX?8 z^K7y`X;NIGZlD}ts_D#{S)G3EB~~^q^VwMai{;$zmUJc(VYbIh;G-Or=&xUn09rCL zEe0mtrefhWhumB9kfYPywlOrFht5pt5!QDHk0pMJh#I{k;T(N+S^|muWoKtQR}a>L zcZxSgv literal 0 HcmV?d00001 diff --git a/internal/timestamp/testdata/TimeStampTokenWithInvalidTSTInfo.p7s b/internal/timestamp/testdata/TimeStampTokenWithInvalidTSTInfo.p7s new file mode 100644 index 0000000000000000000000000000000000000000..153ea92f420c97b73ca803085d437cbf20b75c94 GIT binary patch literal 6578 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 sQMDesSKe1|{558%HALhpY0LlUassI20 literal 0 HcmV?d00001 diff --git a/internal/timestamp/testdata/tsaRootCert.crt b/internal/timestamp/testdata/tsaRootCert.crt new file mode 100644 index 0000000000000000000000000000000000000000..3492b9555d8a927fc048accbdfd510d973a4bf8a GIT binary patch literal 1415 zcmXqLVr@2PV$NQ`%*4pV#OL~KH*>S`;nq}-Xa8LT4S3l&wc0$|zVk9N@~|=(_!ue} z$gwepvTzHFyXWL5CFTTYrspXH<>!|uI6ErnDg>Ds3L6N5RB#D%AymKwnUMtzrbf z7RhT}D$uKbzGw38+~7wZH9Z3C7XQ;Wl_%Ccc-3RGYRi&upQm~WvFHBn+0=Rd!h*!| z6A>jFt|(i5kF`3r==NOBd$BIM6FxSc4WIO@ZchM9vdR7I$rAgr%{Ey+S)*dM{loih zGQ#1Lx82+H<5BDO;zc`5SGj(AFxPBhen{8HqOd%@j-!E2E}~Q3Ek0^7ZML6Z6u0|l zHZ#w1gFS`jPnW$>4}38Fd-9e>uasbWK_l0SS*5b`tlC%^=G|_%ruy>^>pb;H zpEAqnS3j37ywS{m@&A1T!JnWsAklyin0{sX85#exumCd!n*l$FFAUt}2QM&?2T3clNEnDUU{`>etANRnk)c`j?Td?lrv{y!cYtNV z#LQ-Z{64y*oN&oy5m`>wTczfjL3M=6i0;sxcI$G64QH=U6C z7a^^oy&`9ATM4e8`Et>2H?s`?&tUywgCL-KSq* zf7`U1Go!Z543}AQX18c(@Gc!z{wGJ~Np6%dx#}}_*W2&%>s6+*RIFg${jTxpt@xU5 zuZ0szRF`kI^FE;yw)gmn_ZE+0CVi;XtY?ygew1xBAHBZk2sXLxY73M8i@v-TJt6k1Drt4hk L&8$%;mk0m=(y?My literal 0 HcmV?d00001 diff --git a/internal/timestamp/timestamp.go b/internal/timestamp/timestamp.go new file mode 100644 index 00000000..bfb4ce89 --- /dev/null +++ b/internal/timestamp/timestamp.go @@ -0,0 +1,65 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package timestamp provides functionalities of timestamp countersignature +package timestamp + +import ( + "crypto/x509" + + "github.com/notaryproject/notation-core-go/signature" + nx509 "github.com/notaryproject/notation-core-go/x509" + "github.com/notaryproject/tspclient-go" +) + +// Timestamp generates a timestamp request and sends to TSA. It then validates +// the TSA certificate chain against Notary Project certificate and signature +// algorithm requirements. +// On success, it returns the full bytes of the timestamp token received from +// TSA. +// +// Reference: https://github.com/notaryproject/specifications/blob/v1.0.0/specs/signature-specification.md#leaf-certificates +func Timestamp(req *signature.SignRequest, opts tspclient.RequestOptions) ([]byte, error) { + tsaRequest, err := tspclient.NewRequest(opts) + if err != nil { + return nil, err + } + ctx := req.Context() + resp, err := req.Timestamper.Timestamp(ctx, tsaRequest) + if err != nil { + return nil, err + } + token, err := resp.SignedToken() + if err != nil { + return nil, err + } + info, err := token.Info() + if err != nil { + return nil, err + } + timestamp, err := info.Validate(opts.Content) + if err != nil { + return nil, err + } + tsaCertChain, err := token.Verify(ctx, x509.VerifyOptions{ + CurrentTime: timestamp.Value, + Roots: req.TSARootCAs, + }) + if err != nil { + return nil, err + } + if err := nx509.ValidateTimestampingCertChain(tsaCertChain); err != nil { + return nil, err + } + return resp.TimestampToken.FullBytes, nil +} diff --git a/internal/timestamp/timestamp_test.go b/internal/timestamp/timestamp_test.go new file mode 100644 index 00000000..6c1da88f --- /dev/null +++ b/internal/timestamp/timestamp_test.go @@ -0,0 +1,200 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package timestamp + +import ( + "context" + "crypto" + "crypto/x509" + "encoding/asn1" + "errors" + "os" + "strings" + "testing" + + "github.com/notaryproject/notation-core-go/signature" + nx509 "github.com/notaryproject/notation-core-go/x509" + "github.com/notaryproject/tspclient-go" + "github.com/notaryproject/tspclient-go/pki" +) + +const rfc3161TSAurl = "http://rfc3161timestamp.globalsign.com/advanced" + +func TestTimestamp(t *testing.T) { + rootCerts, err := nx509.ReadCertificateFile("testdata/tsaRootCert.crt") + if err != nil || len(rootCerts) == 0 { + t.Fatal("failed to read root CA certificate:", err) + } + rootCert := rootCerts[0] + rootCAs := x509.NewCertPool() + rootCAs.AddCert(rootCert) + + // --------------- Success case ---------------------------------- + timestamper, err := tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl) + if err != nil { + t.Fatal(err) + } + req := &signature.SignRequest{ + Timestamper: timestamper, + TSARootCAs: rootCAs, + } + opts := tspclient.RequestOptions{ + Content: []byte("notation"), + HashAlgorithm: crypto.SHA256, + } + _, err = Timestamp(req, opts) + if err != nil { + t.Fatal(err) + } + + // ------------- Failure cases ------------------------ + opts = tspclient.RequestOptions{ + Content: []byte("notation"), + HashAlgorithm: crypto.SHA1, + } + expectedErr := "malformed timestamping request: unsupported hashing algorithm: SHA-1" + _, err = Timestamp(req, opts) + assertErrorEqual(expectedErr, err, t) + + req = &signature.SignRequest{ + Timestamper: dummyTimestamper{}, + TSARootCAs: rootCAs, + } + opts = tspclient.RequestOptions{ + Content: []byte("notation"), + HashAlgorithm: crypto.SHA256, + NoNonce: true, + } + expectedErr = "failed to timestamp" + _, err = Timestamp(req, opts) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Fatalf("expected error message to contain %s, but got %v", expectedErr, err) + } + + req = &signature.SignRequest{ + Timestamper: dummyTimestamper{ + respWithRejectedStatus: true, + }, + TSARootCAs: rootCAs, + } + expectedErr = "invalid timestamping response: invalid response with status code 2: rejected" + _, err = Timestamp(req, opts) + assertErrorEqual(expectedErr, err, t) + + req = &signature.SignRequest{ + Timestamper: dummyTimestamper{ + invalidTSTInfo: true, + }, + TSARootCAs: rootCAs, + } + expectedErr = "cannot unmarshal TSTInfo from timestamp token: asn1: structure error: tags don't match (23 vs {class:0 tag:16 length:3 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue: tag: stringType:0 timeType:24 set:false omitEmpty:false} Time @89" + _, err = Timestamp(req, opts) + assertErrorEqual(expectedErr, err, t) + + opts = tspclient.RequestOptions{ + Content: []byte("mismatch"), + HashAlgorithm: crypto.SHA256, + NoNonce: true, + } + req = &signature.SignRequest{ + Timestamper: dummyTimestamper{ + failValidate: true, + }, + TSARootCAs: rootCAs, + } + expectedErr = "invalid TSTInfo: mismatched message" + _, err = Timestamp(req, opts) + assertErrorEqual(expectedErr, err, t) + + opts = tspclient.RequestOptions{ + Content: []byte("notation"), + HashAlgorithm: crypto.SHA256, + NoNonce: true, + } + req = &signature.SignRequest{ + Timestamper: dummyTimestamper{ + invalidSignature: true, + }, + TSARootCAs: rootCAs, + } + expectedErr = "failed to verify signed token: cms verification failure: crypto/rsa: verification error" + _, err = Timestamp(req, opts) + assertErrorEqual(expectedErr, err, t) +} + +func assertErrorEqual(expected string, err error, t *testing.T) { + if err == nil || expected != err.Error() { + t.Fatalf("Expected error \"%v\" but was \"%v\"", expected, err) + } +} + +type dummyTimestamper struct { + respWithRejectedStatus bool + invalidTSTInfo bool + failValidate bool + invalidSignature bool +} + +func (d dummyTimestamper) Timestamp(context.Context, *tspclient.Request) (*tspclient.Response, error) { + if d.respWithRejectedStatus { + return &tspclient.Response{ + Status: pki.StatusInfo{ + Status: pki.StatusRejection, + }, + }, nil + } + if d.invalidTSTInfo { + token, err := os.ReadFile("testdata/TimeStampTokenWithInvalidTSTInfo.p7s") + if err != nil { + return nil, err + } + return &tspclient.Response{ + Status: pki.StatusInfo{ + Status: pki.StatusGranted, + }, + TimestampToken: asn1.RawValue{ + FullBytes: token, + }, + }, nil + } + if d.failValidate { + token, err := os.ReadFile("testdata/TimeStampToken.p7s") + if err != nil { + return nil, err + } + return &tspclient.Response{ + Status: pki.StatusInfo{ + Status: pki.StatusGranted, + }, + TimestampToken: asn1.RawValue{ + FullBytes: token, + }, + }, nil + } + if d.invalidSignature { + token, err := os.ReadFile("testdata/TimeStampTokenWithInvalidSignature.p7s") + if err != nil { + return nil, err + } + return &tspclient.Response{ + Status: pki.StatusInfo{ + Status: pki.StatusGranted, + }, + TimestampToken: asn1.RawValue{ + FullBytes: token, + }, + }, nil + } + return nil, errors.New("failed to timestamp") +} diff --git a/revocation/ocsp/ocsp.go b/revocation/ocsp/ocsp.go index d3def3c0..359ce7e9 100644 --- a/revocation/ocsp/ocsp.go +++ b/revocation/ocsp/ocsp.go @@ -36,11 +36,24 @@ import ( "golang.org/x/crypto/ocsp" ) +// Purpose is an enum for purpose of the certificate chain whose OCSP status +// is checked +type Purpose int + +const ( + // PurposeCodeSigning means the certificate chain is a code signing chain + PurposeCodeSigning Purpose = iota + + // PurposeTimestamping means the certificate chain is a timestamping chain + PurposeTimestamping +) + // Options specifies values that are needed to check OCSP revocation type Options struct { - CertChain []*x509.Certificate - SigningTime time.Time - HTTPClient *http.Client + CertChain []*x509.Certificate + CertChainPurpose Purpose // default value is `PurposeCodeSigning` + SigningTime time.Time + HTTPClient *http.Client } const ( @@ -64,8 +77,17 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { // Since this is using authentic signing time, signing time may be zero. // Thus, it is better to pass nil here than fail for a cert's NotBefore // being after zero time - if err := coreX509.ValidateCodeSigningCertChain(opts.CertChain, nil); err != nil { - return nil, result.InvalidChainError{Err: err} + switch opts.CertChainPurpose { + case PurposeCodeSigning: + if err := coreX509.ValidateCodeSigningCertChain(opts.CertChain, nil); err != nil { + return nil, result.InvalidChainError{Err: err} + } + case PurposeTimestamping: + if err := coreX509.ValidateTimestampingCertChain(opts.CertChain); err != nil { + return nil, result.InvalidChainError{Err: err} + } + default: + return nil, result.InvalidChainError{Err: fmt.Errorf("unknown certificate chain purpose %v", opts.CertChainPurpose)} } certResults := make([]*result.CertRevocationResult, len(opts.CertChain)) diff --git a/revocation/ocsp/ocsp_test.go b/revocation/ocsp/ocsp_test.go index 4afca12b..14d43df6 100644 --- a/revocation/ocsp/ocsp_test.go +++ b/revocation/ocsp/ocsp_test.go @@ -474,6 +474,7 @@ func TestCheckStatusErrors(t *testing.T) { noHTTPLeaf.OCSPServer = []string{"ldap://ds.example.com:123/chain_ocsp/0"} noHTTPChain := []*x509.Certificate{noHTTPLeaf, revokableTuples[1].Cert, revokableTuples[2].Cert} + timestampSigningCertErr := result.InvalidChainError{Err: errors.New("timestamp signing certificate with subject \"CN=Notation Test Revokable RSA Chain Cert 3,O=Notary,L=Seattle,ST=WA,C=US\" must have and only have Timestamping as extended key usage")} backwardsChainErr := result.InvalidChainError{Err: errors.New("leaf certificate with subject \"CN=Notation Test Revokable RSA Chain Cert Root,O=Notary,L=Seattle,ST=WA,C=US\" is self-signed. Certificate chain must not contain self-signed leaf certificate")} chainRootErr := result.InvalidChainError{Err: errors.New("root certificate with subject \"CN=Notation Test Revokable RSA Chain Cert 2,O=Notary,L=Seattle,ST=WA,C=US\" is not self-signed. Certificate chain must end with a valid self-signed root certificate")} expiredRespErr := GenericError{Err: errors.New("expired OCSP response")} @@ -531,6 +532,38 @@ func TestCheckStatusErrors(t *testing.T) { } }) + t.Run("check codesigning cert with PurposeTimestamping", func(t *testing.T) { + opts := Options{ + CertChain: okChain, + CertChainPurpose: PurposeTimestamping, + SigningTime: time.Now(), + HTTPClient: http.DefaultClient, + } + certResults, err := CheckStatus(opts) + if err == nil || err.Error() != timestampSigningCertErr.Error() { + t.Errorf("Expected CheckStatus to fail with %v, but got: %v", timestampSigningCertErr, err) + } + if certResults != nil { + t.Error("Expected certResults to be nil when there is an error") + } + }) + + t.Run("check with unknwon CertChainPurpose", func(t *testing.T) { + opts := Options{ + CertChain: okChain, + CertChainPurpose: 2, + SigningTime: time.Now(), + HTTPClient: http.DefaultClient, + } + certResults, err := CheckStatus(opts) + if err == nil || err.Error() != "invalid chain: expected chain to be correct and complete: unknown certificate chain purpose 2" { + t.Errorf("Expected CheckStatus to fail with %v, but got: %v", timestampSigningCertErr, err) + } + if certResults != nil { + t.Error("Expected certResults to be nil when there is an error") + } + }) + t.Run("timeout", func(t *testing.T) { timeoutClient := &http.Client{Timeout: 1 * time.Nanosecond} opts := Options{ diff --git a/revocation/revocation.go b/revocation/revocation.go index 287935bb..801a0780 100644 --- a/revocation/revocation.go +++ b/revocation/revocation.go @@ -35,16 +35,30 @@ type Revocation interface { // revocation is an internal struct used for revocation checking type revocation struct { - httpClient *http.Client + httpClient *http.Client + certChainPurpose ocsp.Purpose } -// New constructs a revocation object +// New constructs a revocation object for code signing certificate chain func New(httpClient *http.Client) (Revocation, error) { if httpClient == nil { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } return &revocation{ - httpClient: httpClient, + httpClient: httpClient, + certChainPurpose: ocsp.PurposeCodeSigning, + }, nil +} + +// NewTimestamp contructs a revocation object for timestamping certificate +// chain +func NewTimestamp(httpClient *http.Client) (Revocation, error) { + if httpClient == nil { + return nil, errors.New("invalid input: a non-nil httpClient must be specified") + } + return &revocation{ + httpClient: httpClient, + certChainPurpose: ocsp.PurposeTimestamping, }, nil } @@ -56,10 +70,12 @@ func New(httpClient *http.Client) (Revocation, error) { // https://github.com/notaryproject/notation-core-go/issues/125 func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { return ocsp.CheckStatus(ocsp.Options{ - CertChain: certChain, - SigningTime: signingTime, - HTTPClient: r.httpClient, + CertChain: certChain, + CertChainPurpose: r.certChainPurpose, + SigningTime: signingTime, + HTTPClient: r.httpClient, }) + // TODO: add CRL support // https://github.com/notaryproject/notation-core-go/issues/125 } diff --git a/revocation/revocation_test.go b/revocation/revocation_test.go index d6b2adb4..f9d4f4e5 100644 --- a/revocation/revocation_test.go +++ b/revocation/revocation_test.go @@ -99,6 +99,14 @@ func TestNew(t *testing.T) { } } +func TestNewTimestamp(t *testing.T) { + expectedErrMsg := "invalid input: a non-nil httpClient must be specified" + _, err := NewTimestamp(nil) + if err == nil || err.Error() != expectedErrMsg { + t.Fatalf("expected %s, but got %s", expectedErrMsg, err) + } +} + func TestCheckRevocationStatusForSingleCert(t *testing.T) { revokableCertTuple := testhelper.GetRevokableRSALeafCertificate() revokableIssuerTuple := testhelper.GetRSARootCertificate() @@ -459,6 +467,239 @@ func TestCheckRevocationStatusForChain(t *testing.T) { }) } +func TestCheckRevocationStatusForTimestampChain(t *testing.T) { + zeroTime := time.Time{} + testChain := testhelper.GetRevokableRSATimestampChain(6) + revokableChain := make([]*x509.Certificate, 6) + for i, tuple := range testChain { + revokableChain[i] = tuple.Cert + revokableChain[i].NotBefore = zeroTime + } + + t.Run("empty chain", func(t *testing.T) { + r, err := NewTimestamp(&http.Client{Timeout: 5 * time.Second}) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate([]*x509.Certificate{}, time.Now()) + expectedErr := result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} + if err == nil || err.Error() != expectedErr.Error() { + t.Errorf("Expected CheckStatus to fail with %v, but got: %v", expectedErr, err) + } + if certResults != nil { + t.Error("Expected certResults to be nil when there is an error") + } + }) + t.Run("check non-revoked chain", func(t *testing.T) { + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + getOKCertResult(revokableChain[2].OCSPServer[0]), + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check chain with 1 Unknown cert", func(t *testing.T) { + // 3rd cert will be unknown, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good}, nil, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + { + Result: result.ResultUnknown, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), + }, + }, + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check OCSP with 1 revoked cert", func(t *testing.T) { + // 3rd cert will be revoked, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + { + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), + }, + }, + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check OCSP with 1 unknown and 1 revoked cert", func(t *testing.T) { + // 3rd cert will be unknown, 5th will be revoked, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + { + Result: result.ResultUnknown, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), + }, + }, + getOKCertResult(revokableChain[3].OCSPServer[0]), + { + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultRevoked, revokableChain[4].OCSPServer[0], revocationocsp.RevokedError{}), + }, + }, + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check OCSP with 1 future revoked cert", func(t *testing.T) { + revokedTime := time.Now().Add(time.Hour) + // 3rd cert will be future revoked, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + getOKCertResult(revokableChain[2].OCSPServer[0]), + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check OCSP with 1 unknown and 1 future revoked cert", func(t *testing.T) { + revokedTime := time.Now().Add(time.Hour) + // 3rd cert will be unknown, 5th will be future revoked, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now()) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + { + Result: result.ResultUnknown, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), + }, + }, + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check OCSP with 1 revoked cert before signing time", func(t *testing.T) { + // 3rd cert will be revoked, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now().Add(time.Hour)) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + { + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), + }, + }, + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) + t.Run("check OCSP with 1 revoked cert after zero signing time", func(t *testing.T) { + revokedTime := time.Now().Add(time.Hour) + // 3rd cert will be revoked, the rest will be good + client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) + if !zeroTime.IsZero() { + t.Errorf("exected zeroTime.IsZero() to be true") + } + r, err := NewTimestamp(client) + if err != nil { + t.Errorf("Expected successful creation of revocation, but received error: %v", err) + } + certResults, err := r.Validate(revokableChain, time.Now().Add(time.Hour)) + if err != nil { + t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) + } + expectedCertResults := []*result.CertRevocationResult{ + getOKCertResult(revokableChain[0].OCSPServer[0]), + getOKCertResult(revokableChain[1].OCSPServer[0]), + { + Result: result.ResultRevoked, + ServerResults: []*result.ServerResult{ + result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), + }, + }, + getOKCertResult(revokableChain[3].OCSPServer[0]), + getOKCertResult(revokableChain[4].OCSPServer[0]), + getRootCertResult(), + } + validateEquivalentCertResults(certResults, expectedCertResults, t) + }) +} + func TestCheckRevocationErrors(t *testing.T) { leafCertTuple := testhelper.GetRSALeafCertificate() rootCertTuple := testhelper.GetRSARootCertificate() diff --git a/signature/cose/conformance_test.go b/signature/cose/conformance_test.go index 37d82243..72b25026 100644 --- a/signature/cose/conformance_test.go +++ b/signature/cose/conformance_test.go @@ -57,7 +57,7 @@ func TestConformance(t *testing.T) { // testSign does conformance check on COSE_Sign1_Tagged func testSign(t *testing.T, sign1 *sign1) { - signRequest, err := getSignReq(sign1) + signRequest, err := getSignReq() if err != nil { t.Fatalf("getSignReq() failed. Error = %s", err) } @@ -90,7 +90,7 @@ func testSign(t *testing.T, sign1 *sign1) { // testVerify does conformance check by decoding COSE_Sign1_Tagged object // into Sign1Message func testVerify(t *testing.T, sign1 *sign1) { - signRequest, err := getSignReq(sign1) + signRequest, err := getSignReq() if err != nil { t.Fatalf("getSignReq() failed. Error = %s", err) } @@ -124,7 +124,7 @@ func testVerify(t *testing.T, sign1 *sign1) { verifySignerInfo(&content.SignerInfo, signRequest, t) } -func getSignReq(sign1 *sign1) (*signature.SignRequest, error) { +func getSignReq() (*signature.SignRequest, error) { certs := []*x509.Certificate{testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert} signer, err := signature.NewLocalSigner(certs, testhelper.GetRSALeafCertificate().PrivateKey) if err != nil { diff --git a/signature/cose/envelope.go b/signature/cose/envelope.go index 0c461d71..d5b7b247 100644 --- a/signature/cose/envelope.go +++ b/signature/cose/envelope.go @@ -24,8 +24,10 @@ import ( "time" "github.com/fxamacker/cbor/v2" + "github.com/notaryproject/notation-core-go/internal/timestamp" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/internal/base" + "github.com/notaryproject/tspclient-go" "github.com/veraison/go-cose" ) @@ -76,7 +78,7 @@ const ( // Unprotected Headers // https://github.com/notaryproject/notaryproject/blob/cose-envelope/signature-envelope-cose.md const ( - headerLabelTimeStampSignature = "io.cncf.notary.timestampSignature" + headerLabelTimestampSignature = "io.cncf.notary.timestampSignature" headerLabelSigningAgent = "io.cncf.notary.signingAgent" ) @@ -234,10 +236,26 @@ func (e *envelope) Sign(req *signature.SignRequest) ([]byte, error) { return nil, &signature.InvalidSignRequestError{Msg: err.Error()} } - // generate unprotected headers of COSE envelope + // generate unprotected headers of COSE envelope. generateUnprotectedHeaders(req, signer, msg.Headers.Unprotected) - // TODO: needs to add headerKeyTimeStampSignature. + // timestamping + if req.SigningScheme == signature.SigningSchemeX509 && req.Timestamper != nil { + hash, err := hashFromCOSEAlgorithm(signer.Algorithm()) + if err != nil { + return nil, &signature.TimestampError{Detail: err} + } + timestampOpts := tspclient.RequestOptions{ + Content: msg.Signature, + HashAlgorithm: hash, + } + timestampToken, err := timestamp.Timestamp(req, timestampOpts) + if err != nil { + return nil, &signature.TimestampError{Detail: err} + } + // on success, embed the timestamp token to Unprotected header + msg.Headers.Unprotected[headerLabelTimestampSignature] = timestampToken + } // encode Sign1Message into COSE_Sign1_Tagged object encoded, err := msg.MarshalCBOR() @@ -368,7 +386,10 @@ func (e *envelope) signerInfo() (*signature.SignerInfo, error) { signerInfo.UnsignedAttributes.SigningAgent = h } - // TODO: needs to add headerKeyTimeStampSignature. + // populate signerInfo.UnsignedAttributes.TimestampSignature + if timestamepToken, ok := e.base.Headers.Unprotected[headerLabelTimestampSignature].([]byte); ok { + signerInfo.UnsignedAttributes.TimestampSignature = timestamepToken + } return &signerInfo, nil } @@ -701,3 +722,17 @@ func generateRawProtectedCBORMap(rawProtected cbor.RawMessage) (map[any]cbor.Raw return headerMap, nil } + +// hashFromCOSEAlgorithm maps the cose algorithm supported by go-cose to hash +func hashFromCOSEAlgorithm(alg cose.Algorithm) (crypto.Hash, error) { + switch alg { + case cose.AlgorithmPS256, cose.AlgorithmES256: + return crypto.SHA256, nil + case cose.AlgorithmPS384, cose.AlgorithmES384: + return crypto.SHA384, nil + case cose.AlgorithmPS512, cose.AlgorithmES512: + return crypto.SHA512, nil + default: + return 0, fmt.Errorf("unsupported cose algorithm %s", alg) + } +} diff --git a/signature/cose/envelope_test.go b/signature/cose/envelope_test.go index e91697b3..b9f2c11c 100644 --- a/signature/cose/envelope_test.go +++ b/signature/cose/envelope_test.go @@ -25,11 +25,15 @@ import ( "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/internal/signaturetest" "github.com/notaryproject/notation-core-go/testhelper" + nx509 "github.com/notaryproject/notation-core-go/x509" + "github.com/notaryproject/tspclient-go" "github.com/veraison/go-cose" ) const ( payloadString = "{\"targetArtifact\":{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"digest\":\"sha256:73c803930ea3ba1e54bc25c2bdc53edd0284c62ed651fe7b00369da519a3c333\",\"size\":16724,\"annotations\":{\"io.wabbit-networks.buildId\":\"123\"}}}" + + rfc3161TSAurl = "http://rfc3161timestamp.globalsign.com/advanced" ) var ( @@ -123,6 +127,49 @@ func TestSign(t *testing.T) { } } } + + t.Run("with timestmap countersignature request", func(t *testing.T) { + signRequest, err := newSignRequest("notary.x509", signature.KeyTypeRSA, 3072) + if err != nil { + t.Fatalf("newSignRequest() failed. Error = %s", err) + } + signRequest.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl) + if err != nil { + t.Fatal(err) + } + rootCerts, err := nx509.ReadCertificateFile("../../internal/timestamp/testdata/tsaRootCert.crt") + if err != nil || len(rootCerts) == 0 { + t.Fatal("failed to read root CA certificate:", err) + } + rootCert := rootCerts[0] + rootCAs := x509.NewCertPool() + rootCAs.AddCert(rootCert) + signRequest.TSARootCAs = rootCAs + encoded, err := env.Sign(signRequest) + if err != nil || encoded == nil { + t.Fatalf("Sign() failed. Error = %s", err) + } + content, err := env.Content() + if err != nil { + t.Fatal(err) + } + timestampToken := content.SignerInfo.UnsignedAttributes.TimestampSignature + if len(timestampToken) == 0 { + t.Fatal("expected timestamp token to be present") + } + signedToken, err := tspclient.ParseSignedToken(timestampToken) + if err != nil { + t.Fatal(err) + } + info, err := signedToken.Info() + if err != nil { + t.Fatal(err) + } + _, err = info.Validate(content.SignerInfo.Signature) + if err != nil { + t.Fatal(err) + } + }) } func TestSignErrors(t *testing.T) { @@ -288,6 +335,29 @@ func TestSignErrors(t *testing.T) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) + + t.Run("when invalid tsa url is provided", func(t *testing.T) { + signRequest, err := getSignRequest() + if err != nil { + t.Fatalf("getSignRequest() failed. Error = %v", err) + } + signRequest.Timestamper, err = tspclient.NewHTTPTimestamper(nil, "invalid") + if err != nil { + t.Fatal(err) + } + expected := errors.New("timestamp: Post \"invalid\": unsupported protocol scheme \"\"") + encoded, err := env.Sign(signRequest) + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + var timestampErr *signature.TimestampError + if !errors.As(err, ×tampErr) { + t.Fatal("expected signature.TimestampError") + } + if encoded != nil { + t.Fatal("expected nil signature envelope") + } + }) } func TestVerifyErrors(t *testing.T) { @@ -801,6 +871,44 @@ func TestGenerateExtendedAttributesError(t *testing.T) { } } +func TestHashFunc(t *testing.T) { + hash, err := hashFromCOSEAlgorithm(cose.AlgorithmPS256) + if err != nil || hash.String() != "SHA-256" { + t.Fatalf("expected SHA-256, but got %s", hash) + } + + hash, err = hashFromCOSEAlgorithm(cose.AlgorithmPS384) + if err != nil || hash.String() != "SHA-384" { + t.Fatalf("expected SHA-384, but got %s", hash) + } + + hash, err = hashFromCOSEAlgorithm(cose.AlgorithmPS512) + if err != nil || hash.String() != "SHA-512" { + t.Fatalf("expected SHA-512, but got %s", hash) + } + + hash, err = hashFromCOSEAlgorithm(cose.AlgorithmES256) + if err != nil || hash.String() != "SHA-256" { + t.Fatalf("expected SHA-256, but got %s", hash) + } + + hash, err = hashFromCOSEAlgorithm(cose.AlgorithmES384) + if err != nil || hash.String() != "SHA-384" { + t.Fatalf("expected SHA-384, but got %s", hash) + } + + hash, err = hashFromCOSEAlgorithm(cose.AlgorithmES512) + if err != nil || hash.String() != "SHA-512" { + t.Fatalf("expected SHA-512, but got %s", hash) + } + + _, err = hashFromCOSEAlgorithm(cose.AlgorithmEd25519) + expectedErrMsg := "unsupported cose algorithm EdDSA" + if err == nil || err.Error() != expectedErrMsg { + t.Fatalf("expected %s, but got %s", expectedErrMsg, err) + } +} + func newSignRequest(signingScheme string, keyType signature.KeyType, size int) (*signature.SignRequest, error) { signer, err := signaturetest.GetTestLocalSigner(keyType, size) if err != nil { diff --git a/signature/errors.go b/signature/errors.go index d106c180..ead7c2c1 100644 --- a/signature/errors.go +++ b/signature/errors.go @@ -142,3 +142,28 @@ type DuplicateKeyError struct { func (e *DuplicateKeyError) Error() string { return fmt.Sprintf("repeated key: %q exists.", e.Key) } + +// TimestampError is any error related to RFC3161 Timestamp. +type TimestampError struct { + Msg string + Detail error +} + +// Error returns the formatted error message. +func (e *TimestampError) Error() string { + if e.Msg != "" && e.Detail != nil { + return fmt.Sprintf("timestamp: %s. Error: %s", e.Msg, e.Detail.Error()) + } + if e.Msg != "" { + return fmt.Sprintf("timestamp: %s", e.Msg) + } + if e.Detail != nil { + return fmt.Sprintf("timestamp: %s", e.Detail.Error()) + } + return "timestamp error" +} + +// Unwrap returns the detail error of e. +func (e *TimestampError) Unwrap() error { + return e.Detail +} diff --git a/signature/errors_test.go b/signature/errors_test.go index 5662c50f..da2fbe74 100644 --- a/signature/errors_test.go +++ b/signature/errors_test.go @@ -177,3 +177,34 @@ func TestEnvelopeKeyRepeatedError(t *testing.T) { t.Errorf("Expected %v but got %v", expectMsg, err.Error()) } } + +func TestTimestampError(t *testing.T) { + err := &TimestampError{Msg: "test error", Detail: errors.New("test inner error")} + expectMsg := "timestamp: test error. Error: test inner error" + if err.Error() != expectMsg { + t.Errorf("Expected %v but got %v", expectMsg, err.Error()) + } + + err = &TimestampError{Msg: "test error"} + expectMsg = "timestamp: test error" + if err.Error() != expectMsg { + t.Errorf("Expected %v but got %v", expectMsg, err.Error()) + } + + err = &TimestampError{Detail: errors.New("test inner error")} + expectMsg = "timestamp: test inner error" + if err.Error() != expectMsg { + t.Errorf("Expected %v but got %v", expectMsg, err.Error()) + } + unwrappedErr := err.Unwrap() + expectMsg = "test inner error" + if unwrappedErr.Error() != expectMsg { + t.Errorf("Expected %s but got %s", errMsg, unwrappedErr.Error()) + } + + err = &TimestampError{} + expectMsg = "timestamp error" + if err.Error() != expectMsg { + t.Errorf("Expected %v but got %v", expectMsg, err.Error()) + } +} diff --git a/signature/internal/base/envelope.go b/signature/internal/base/envelope.go index 41c685e0..8a7609bf 100644 --- a/signature/internal/base/envelope.go +++ b/signature/internal/base/envelope.go @@ -53,7 +53,6 @@ func (e *Envelope) Sign(req *signature.SignRequest) ([]byte, error) { if err != nil { return nil, err } - if err := validateCertificateChain( content.SignerInfo.CertificateChain, &content.SignerInfo.SignedAttributes.SigningTime, @@ -62,7 +61,9 @@ func (e *Envelope) Sign(req *signature.SignRequest) ([]byte, error) { return nil, err } + // store the raw signature e.Raw = raw + return e.Raw, nil } diff --git a/signature/internal/base/envelope_test.go b/signature/internal/base/envelope_test.go index e0be51c3..1498a0f8 100644 --- a/signature/internal/base/envelope_test.go +++ b/signature/internal/base/envelope_test.go @@ -22,10 +22,12 @@ import ( "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/testhelper" + "github.com/notaryproject/tspclient-go" ) var ( errMsg = "error msg" + invalidTimestamper tspclient.Timestamper invalidSigningAgent = "test/1" validSigningAgent = "test/0" invalidContentType = "text/plain" @@ -35,13 +37,13 @@ var ( time08_02 time.Time time08_03 time.Time timeLayout = "2006-01-02" - signiningSchema = signature.SigningScheme("notary.x509") + signiningSchema = signature.SigningScheme("notary.x509") validSignerInfo = &signature.SignerInfo{ Signature: validBytes, SignatureAlgorithm: signature.AlgorithmPS384, SignedAttributes: signature.SignedAttributes{ - SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, - Expiry: testhelper.GetECLeafCertificate().Cert.NotAfter, + SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, + Expiry: testhelper.GetECLeafCertificate().Cert.NotAfter, SigningScheme: signiningSchema, }, CertificateChain: []*x509.Certificate{ @@ -62,8 +64,8 @@ var ( ContentType: validContentType, Content: validBytes, }, - SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, - Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, + SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, + Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, SigningScheme: signiningSchema, Signer: &mockSigner{ keySpec: signature.KeySpec{ @@ -82,8 +84,8 @@ var ( ContentType: validContentType, Content: validBytes, }, - SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, - Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, + SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, + Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, SigningScheme: signiningSchema, Signer: &mockSigner{ keySpec: signature.KeySpec{ @@ -97,19 +99,42 @@ var ( }, SigningAgent: invalidSigningAgent, } + reqWithInvalidTSAurl = &signature.SignRequest{ + Payload: signature.Payload{ + ContentType: validContentType, + Content: validBytes, + }, + SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, + Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, + SigningScheme: signiningSchema, + Signer: &mockSigner{ + keySpec: signature.KeySpec{ + Type: signature.KeyTypeRSA, + Size: 3072, + }, + certs: []*x509.Certificate{ + testhelper.GetRSALeafCertificate().Cert, + testhelper.GetRSARootCertificate().Cert, + }, + }, + SigningAgent: validSigningAgent, + Timestamper: invalidTimestamper, + } ) func init() { time08_02, _ = time.Parse(timeLayout, "2020-08-02") time08_03, _ = time.Parse(timeLayout, "2020-08-03") + invalidTimestamper, _ = tspclient.NewHTTPTimestamper(nil, "invalid") } // Mock an internal envelope that implements signature.Envelope. type mockEnvelope struct { - payload *signature.Payload - signerInfo *signature.SignerInfo - content *signature.EnvelopeContent - failVerify bool + payload *signature.Payload + signerInfo *signature.SignerInfo + content *signature.EnvelopeContent + failTimestamp bool + failVerify bool } // Sign implements Sign of signature.Envelope. @@ -118,6 +143,9 @@ func (e mockEnvelope) Sign(req *signature.SignRequest) ([]byte, error) { case invalidSigningAgent: return nil, errors.New(errMsg) case validSigningAgent: + if e.failTimestamp { + return validBytes, &signature.TimestampError{} + } return validBytes, nil } return nil, nil @@ -234,6 +262,21 @@ func TestSign(t *testing.T) { expect: validBytes, expectErr: false, }, + { + name: "failed to timestamp", + req: reqWithInvalidTSAurl, + env: &Envelope{ + Raw: validBytes, + Envelope: &mockEnvelope{ + content: &signature.EnvelopeContent{ + SignerInfo: *validSignerInfo, + }, + failTimestamp: true, + }, + }, + expect: nil, + expectErr: true, + }, } for _, tt := range tests { @@ -246,6 +289,17 @@ func TestSign(t *testing.T) { if !reflect.DeepEqual(sig, tt.expect) { t.Errorf("expect %+v, got %+v", tt.expect, sig) } + + if tt.name == "failed to timestamp" { + var timestampErr *signature.TimestampError + if !errors.As(err, ×tampErr) { + t.Fatal("expecting error to be signature.TimestampError") + } + expectedErrMsg := "timestamp error" + if timestampErr.Error() != expectedErrMsg { + t.Fatalf("expected error %s, but got %v", expectedErrMsg, err) + } + } }) } } @@ -456,8 +510,8 @@ func TestValidateSignRequest(t *testing.T) { ContentType: validContentType, Content: validBytes, }, - SigningTime: time08_02, - Expiry: time08_03, + SigningTime: time08_02, + Expiry: time08_03, SigningScheme: signiningSchema, Signer: &mockSigner{ certs: []*x509.Certificate{ diff --git a/signature/jws/envelope.go b/signature/jws/envelope.go index eb280488..f0bcd2c0 100644 --- a/signature/jws/envelope.go +++ b/signature/jws/envelope.go @@ -20,8 +20,10 @@ import ( "fmt" "github.com/golang-jwt/jwt/v4" + "github.com/notaryproject/notation-core-go/internal/timestamp" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/internal/base" + "github.com/notaryproject/tspclient-go" ) // MediaTypeEnvelope defines the media type name of JWS envelope. @@ -91,11 +93,17 @@ func (e *envelope) Sign(req *signature.SignRequest) ([]byte, error) { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } + // timestamping + if err := timestampJWS(env, req, signedAttrs[headerKeySigningScheme].(string)); err != nil { + return nil, err + } + encoded, err := json.Marshal(env) if err != nil { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } e.base = env + return encoded, nil } @@ -220,3 +228,34 @@ func sign(payload jwt.MapClaims, headers map[string]interface{}, method signingM } return compact, certs, nil } + +// timestampJWS timestamps a JWS envelope +func timestampJWS(env *jwsEnvelope, req *signature.SignRequest, signingScheme string) error { + if signingScheme != string(signature.SigningSchemeX509) || req.Timestamper == nil { + return nil + } + primitiveSignature, err := base64.RawURLEncoding.DecodeString(env.Signature) + if err != nil { + return &signature.TimestampError{Detail: err} + } + ks, err := req.Signer.KeySpec() + if err != nil { + return &signature.TimestampError{Detail: err} + } + hash := ks.SignatureAlgorithm().Hash() + if hash == 0 { + return &signature.TimestampError{Msg: fmt.Sprintf("got hash value 0 from key spec %+v", ks)} + } + timestampOpts := tspclient.RequestOptions{ + Content: primitiveSignature, + HashAlgorithm: hash, + } + timestampToken, err := timestamp.Timestamp(req, timestampOpts) + if err != nil { + return &signature.TimestampError{Detail: err} + } + + // on success, embed the timestamp token to TimestampSignature + env.Header.TimestampSignature = timestampToken + return nil +} diff --git a/signature/jws/envelope_test.go b/signature/jws/envelope_test.go index f93643bf..4d765165 100644 --- a/signature/jws/envelope_test.go +++ b/signature/jws/envelope_test.go @@ -32,8 +32,12 @@ import ( "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/internal/signaturetest" "github.com/notaryproject/notation-core-go/testhelper" + nx509 "github.com/notaryproject/notation-core-go/x509" + "github.com/notaryproject/tspclient-go" ) +const rfc3161TSAurl = "http://rfc3161timestamp.globalsign.com/advanced" + // remoteMockSigner is used to mock remote signer type remoteMockSigner struct { privateKey crypto.PrivateKey @@ -226,8 +230,8 @@ func TestNewEnvelope(t *testing.T) { } } -// Test the same key exists both in extended signed attributes and protected header func TestSignFailed(t *testing.T) { + // Test the same key exists both in extended signed attributes and protected header t.Run("extended attribute conflict with protected header keys", func(t *testing.T) { _, err := getEncodedMessage(signature.SigningSchemeX509, true, extSignedAttrRepeated) checkErrorEqual(t, "attribute key:cty repeated", err.Error()) @@ -253,6 +257,32 @@ func TestSignFailed(t *testing.T) { _, err = e.Sign(signReq) checkErrorEqual(t, `signature algorithm "#0" is not supported`, err.Error()) }) + + t.Run("invalid tsa url", func(t *testing.T) { + env := envelope{} + signer, err := signaturetest.GetTestLocalSigner(signature.KeyTypeRSA, 3072) + checkNoError(t, err) + + signReq, err := getSignReq(signature.SigningSchemeX509, signer, nil) + checkNoError(t, err) + + signReq.Timestamper, err = tspclient.NewHTTPTimestamper(nil, "invalid") + if err != nil { + t.Fatal(err) + } + expected := errors.New("timestamp: Post \"invalid\": unsupported protocol scheme \"\"") + encoded, err := env.Sign(signReq) + if !isErrEqual(expected, err) { + t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) + } + var timestampErr *signature.TimestampError + if !errors.As(err, ×tampErr) { + t.Fatal("expected signature.TimestampError") + } + if encoded != nil { + t.Fatal("expected nil signature envelope") + } + }) } func TestSigningScheme(t *testing.T) { @@ -302,6 +332,52 @@ func TestSignVerify(t *testing.T) { } } +func TestSignWithTimestamp(t *testing.T) { + signer, err := signaturetest.GetTestLocalSigner(signature.KeyTypeRSA, 3072) + checkNoError(t, err) + + signReq, err := getSignReq(signature.SigningSchemeX509, signer, nil) + checkNoError(t, err) + + signReq.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl) + if err != nil { + t.Fatal(err) + } + rootCerts, err := nx509.ReadCertificateFile("../../internal/timestamp/testdata/tsaRootCert.crt") + if err != nil || len(rootCerts) == 0 { + t.Fatal("failed to read root CA certificate:", err) + } + rootCert := rootCerts[0] + rootCAs := x509.NewCertPool() + rootCAs.AddCert(rootCert) + signReq.TSARootCAs = rootCAs + env := envelope{} + encoded, err := env.Sign(signReq) + if err != nil || encoded == nil { + t.Fatalf("Sign() failed. Error = %s", err) + } + content, err := env.Content() + if err != nil { + t.Fatal(err) + } + timestampToken := content.SignerInfo.UnsignedAttributes.TimestampSignature + if len(timestampToken) == 0 { + t.Fatal("expected timestamp token to be present") + } + signedToken, err := tspclient.ParseSignedToken(timestampToken) + if err != nil { + t.Fatal(err) + } + info, err := signedToken.Info() + if err != nil { + t.Fatal(err) + } + _, err = info.Validate(content.SignerInfo.Signature) + if err != nil { + t.Fatal(err) + } +} + func TestVerify(t *testing.T) { t.Run("break json format", func(t *testing.T) { encoded, err := getEncodedMessage(signature.SigningSchemeX509, true, extSignedAttr) @@ -601,3 +677,13 @@ func TestEmptyEnvelope(t *testing.T) { } }) } + +func isErrEqual(wanted, got error) bool { + if wanted == nil && got == nil { + return true + } + if wanted != nil && got != nil { + return wanted.Error() == got.Error() + } + return false +} diff --git a/signature/jws/types.go b/signature/jws/types.go index bfbb204b..b2e34455 100644 --- a/signature/jws/types.go +++ b/signature/jws/types.go @@ -73,7 +73,7 @@ type jwsProtectedHeader struct { // jwsUnprotectedHeader contains the set of unprotected headers. type jwsUnprotectedHeader struct { - // RFC3161 time stamp token Base64-encoded. + // RFC3161 timestamp token Base64-encoded. TimestampSignature []byte `json:"io.cncf.notary.timestampSignature,omitempty"` // List of X.509 Base64-DER-encoded certificates diff --git a/signature/types.go b/signature/types.go index e324b92f..ab53bee8 100644 --- a/signature/types.go +++ b/signature/types.go @@ -14,9 +14,12 @@ package signature import ( + "context" "crypto/x509" "errors" "time" + + "github.com/notaryproject/tspclient-go" ) // SignatureMediaType list the supported media-type for signatures. @@ -101,6 +104,41 @@ type SignRequest struct { // SigningScheme defines the Notary Project Signing Scheme used by the signature. SigningScheme SigningScheme + + // Timestamper denotes the timestamper for RFC 3161 timestamping + Timestamper tspclient.Timestamper + + // TSARootCAs is the set of caller trusted TSA root certificates + TSARootCAs *x509.CertPool + + // ctx is the caller context. It should only be modified via WithContext. + // It is unexported to prevent people from using Context wrong + // and mutating the contexts held by callers of the same request. + ctx context.Context +} + +// Context returns the SignRequest's context. To change the context, use +// [SignRequest.WithContext]. +// +// The returned context is always non-nil; it defaults to the +// background context. +func (r *SignRequest) Context() context.Context { + if r.ctx != nil { + return r.ctx + } + return context.Background() +} + +// WithContext returns a shallow copy of r with its context changed +// to ctx. The provided ctx must be non-nil. +func (r *SignRequest) WithContext(ctx context.Context) *SignRequest { + if ctx == nil { + panic("nil context") + } + r2 := new(SignRequest) + *r2 = *r + r2.ctx = ctx + return r2 } // EnvelopeContent represents a combination of payload to be signed and a parsed diff --git a/signature/types_test.go b/signature/types_test.go new file mode 100644 index 00000000..f8ff5625 --- /dev/null +++ b/signature/types_test.go @@ -0,0 +1,53 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package signature + +import ( + "context" + "fmt" + "testing" +) + +func TestSignRequestContext(t *testing.T) { + r := &SignRequest{ + ctx: context.WithValue(context.Background(), "k1", "v1"), + } + + ctx := r.Context() + if ctx.Value("k1") != "v1" { + t.Fatal("expected k1:v1 in ctx") + } + + r = &SignRequest{} + ctx = r.Context() + if fmt.Sprint(ctx) != "context.Background" { + t.Fatal("expected context.Background") + } +} + +func TestSignRequestWithContext(t *testing.T) { + r := &SignRequest{} + ctx := context.WithValue(context.Background(), "k1", "v1") + r = r.WithContext(ctx) + if r.ctx.Value("k1") != "v1" { + t.Fatal("expected k1:v1 in request ctx") + } + + defer func() { + if rc := recover(); rc == nil { + t.Errorf("expected to be panic") + } + }() + r.WithContext(nil) // should panic +} diff --git a/testhelper/certificatetest.go b/testhelper/certificatetest.go index 1e9eb0e3..54a31c0e 100644 --- a/testhelper/certificatetest.go +++ b/testhelper/certificatetest.go @@ -22,12 +22,15 @@ import ( "crypto/rsa" "crypto/x509" "crypto/x509/pkix" + "encoding/asn1" "fmt" "math/big" mrand "math/rand" "strconv" "sync" "time" + + "github.com/notaryproject/notation-core-go/internal/oid" ) var ( @@ -81,7 +84,22 @@ func GetRevokableRSAChain(size int) []RSACertTuple { chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i) } if size > 1 { - chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0) + chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, true, false) + } + return chain +} + +// GetRevokableRSATimestampChain returns a chain of certificates that specify a local OCSP server signed using RSA algorithm. +// The leaf certificate is a timestamp certificate. +func GetRevokableRSATimestampChain(size int) []RSACertTuple { + setupCertificates() + chain := make([]RSACertTuple, size) + chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1) + for i := size - 2; i > 0; i-- { + chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i) + } + if size > 1 { + chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, false, true) } return chain } @@ -148,13 +166,13 @@ func getRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple { } func getRevokableRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple { - template := getCertTemplate(issuer == nil, true, cn) + template := getCertTemplate(issuer == nil, true, false, cn) template.OCSPServer = []string{"http://example.com/ocsp"} return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer) } func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int) RSACertTuple { - template := getCertTemplate(previous == nil, true, cn) + template := getCertTemplate(previous == nil, true, false, cn) template.BasicConstraintsValid = true template.IsCA = true template.KeyUsage = x509.KeyUsageCertSign @@ -164,7 +182,7 @@ func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int) func getRevokableRSARootChainCertTuple(cn string, pathLen int) RSACertTuple { pk, _ := rsa.GenerateKey(rand.Reader, 3072) - template := getCertTemplate(true, true, cn) + template := getCertTemplate(true, true, false, cn) template.BasicConstraintsValid = true template.IsCA = true template.KeyUsage = x509.KeyUsageCertSign @@ -172,8 +190,8 @@ func getRevokableRSARootChainCertTuple(cn string, pathLen int) RSACertTuple { return getRSACertTupleWithTemplate(template, pk, nil) } -func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int) RSACertTuple { - template := getCertTemplate(false, true, cn) +func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int, codesign, timestamp bool) RSACertTuple { + template := getCertTemplate(false, codesign, timestamp, cn) template.BasicConstraintsValid = true template.IsCA = false template.KeyUsage = x509.KeyUsageDigitalSignature @@ -183,7 +201,7 @@ func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index in func getRSACertWithoutEKUTuple(cn string, issuer *RSACertTuple) RSACertTuple { pk, _ := rsa.GenerateKey(rand.Reader, 3072) - template := getCertTemplate(issuer == nil, false, cn) + template := getCertTemplate(issuer == nil, false, false, cn) return getRSACertTupleWithTemplate(template, pk, issuer) } @@ -200,18 +218,18 @@ func getECCertTuple(cn string, issuer *ECCertTuple) ECCertTuple { func GetRSASelfSignedSigningCertTuple(cn string) RSACertTuple { // Even though we are creating self-signed root, we are using false for 'isRoot' to not // add root CA's basic constraint, KU and EKU. - template := getCertTemplate(false, true, cn) + template := getCertTemplate(false, true, false, cn) privKey, _ := rsa.GenerateKey(rand.Reader, 3072) return getRSACertTupleWithTemplate(template, privKey, nil) } func GetRSACertTupleWithPK(privKey *rsa.PrivateKey, cn string, issuer *RSACertTuple) RSACertTuple { - template := getCertTemplate(issuer == nil, true, cn) + template := getCertTemplate(issuer == nil, true, false, cn) return getRSACertTupleWithTemplate(template, privKey, issuer) } func GetRSASelfSignedCertTupleWithPK(privKey *rsa.PrivateKey, cn string) RSACertTuple { - template := getCertTemplate(false, true, cn) + template := getCertTemplate(false, true, false, cn) return getRSACertTupleWithTemplate(template, privKey, nil) } @@ -231,7 +249,7 @@ func getRSACertTupleWithTemplate(template *x509.Certificate, privKey *rsa.Privat } func GetECDSACertTupleWithPK(privKey *ecdsa.PrivateKey, cn string, issuer *ECCertTuple) ECCertTuple { - template := getCertTemplate(issuer == nil, true, cn) + template := getCertTemplate(issuer == nil, true, false, cn) var certBytes []byte if issuer != nil { @@ -247,7 +265,7 @@ func GetECDSACertTupleWithPK(privKey *ecdsa.PrivateKey, cn string, issuer *ECCer } } -func getCertTemplate(isRoot bool, setCodeSignEKU bool, cn string) *x509.Certificate { +func getCertTemplate(isRoot bool, setCodeSignEKU, setTimestampEKU bool, cn string) *x509.Certificate { template := &x509.Certificate{ Subject: pkix.Name{ Organization: []string{"Notary"}, @@ -262,6 +280,15 @@ func getCertTemplate(isRoot bool, setCodeSignEKU bool, cn string) *x509.Certific if setCodeSignEKU { template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning} + } else if setTimestampEKU { + ekuValue, _ := asn1.Marshal([]asn1.ObjectIdentifier{oid.Timestamping}) + template.ExtraExtensions = []pkix.Extension{ + { + Id: oid.ExtKeyUsage, + Critical: true, + Value: ekuValue, + }, + } } if isRoot { @@ -275,7 +302,6 @@ func getCertTemplate(isRoot bool, setCodeSignEKU bool, cn string) *x509.Certific template.SerialNumber = big.NewInt(int64(mrand.Intn(200))) template.NotAfter = time.Now().AddDate(0, 0, 1) } - return template } diff --git a/x509/cert_validations.go b/x509/cert_validations.go deleted file mode 100644 index 8dd7d0ed..00000000 --- a/x509/cert_validations.go +++ /dev/null @@ -1,306 +0,0 @@ -// Copyright The Notary Project Authors. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package x509 - -import ( - "bytes" - "crypto/ecdsa" - "crypto/rsa" - "crypto/x509" - "errors" - "fmt" - "strings" - "time" -) - -// ValidateCodeSigningCertChain takes an ordered code-signing certificate chain -// and validates issuance from leaf to root -// Validates certificates according to this spec: -// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#certificate-requirements -func ValidateCodeSigningCertChain(certChain []*x509.Certificate, signingTime *time.Time) error { - return validateCertChain(certChain, 0, signingTime) -} - -// ValidateTimeStampingCertChain takes an ordered time-stamping certificate -// chain and validates issuance from leaf to root -// Validates certificates according to this spec: -// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#certificate-requirements -func ValidateTimeStampingCertChain(certChain []*x509.Certificate, signingTime *time.Time) error { - return validateCertChain(certChain, x509.ExtKeyUsageTimeStamping, signingTime) -} - -func validateCertChain(certChain []*x509.Certificate, expectedLeafEku x509.ExtKeyUsage, signingTime *time.Time) error { - if len(certChain) < 1 { - return errors.New("certificate chain must contain at least one certificate") - } - - // For self-signed signing certificate (not a CA) - if len(certChain) == 1 { - cert := certChain[0] - if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil { - return signedTimeError - } - if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { - return fmt.Errorf("invalid self-signed certificate. subject: %q. Error: %w", cert.Subject, err) - } - if err := validateLeafCertificate(cert, expectedLeafEku); err != nil { - return fmt.Errorf("invalid self-signed certificate. Error: %w", err) - } - return nil - } - - for i, cert := range certChain { - if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil { - return signedTimeError - } - if i == len(certChain)-1 { - selfSigned, selfSignedError := isSelfSigned(cert) - if selfSignedError != nil { - return fmt.Errorf("root certificate with subject %q is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: %v", cert.Subject, selfSignedError) - } - if !selfSigned { - return fmt.Errorf("root certificate with subject %q is not self-signed. Certificate chain must end with a valid self-signed root certificate", cert.Subject) - } - } else { - // This is to avoid extra/redundant multiple root cert at the end - // of certificate-chain - selfSigned, selfSignedError := isSelfSigned(cert) - // not checking selfSignedError != nil here because we expect - // a non-nil err. For a non-root certificate, it shouldn't be - // self-signed, hence CheckSignatureFrom would return a non-nil - // error. - if selfSignedError == nil && selfSigned { - if i == 0 { - return fmt.Errorf("leaf certificate with subject %q is self-signed. Certificate chain must not contain self-signed leaf certificate", cert.Subject) - } - return fmt.Errorf("intermediate certificate with subject %q is self-signed. Certificate chain must not contain self-signed intermediate certificate", cert.Subject) - } - parentCert := certChain[i+1] - issuedBy, issuedByError := isIssuedBy(cert, parentCert) - if issuedByError != nil { - return fmt.Errorf("invalid certificates or certificate with subject %q is not issued by %q. Error: %v", cert.Subject, parentCert.Subject, issuedByError) - } - if !issuedBy { - return fmt.Errorf("certificate with subject %q is not issued by %q", cert.Subject, parentCert.Subject) - } - } - - if i == 0 { - if err := validateLeafCertificate(cert, expectedLeafEku); err != nil { - return err - } - } else { - if err := validateCACertificate(cert, i-1); err != nil { - return err - } - } - } - return nil -} - -func isSelfSigned(cert *x509.Certificate) (bool, error) { - return isIssuedBy(cert, cert) -} - -func isIssuedBy(subject *x509.Certificate, issuer *x509.Certificate) (bool, error) { - if err := subject.CheckSignatureFrom(issuer); err != nil { - return false, err - } - return bytes.Equal(issuer.RawSubject, subject.RawIssuer), nil -} - -func validateSigningTime(cert *x509.Certificate, signingTime *time.Time) error { - if signingTime != nil && (signingTime.Before(cert.NotBefore) || signingTime.After(cert.NotAfter)) { - return fmt.Errorf("certificate with subject %q was invalid at signing time of %s. Certificate is valid from [%s] to [%s]", - cert.Subject, signingTime.UTC(), cert.NotBefore.UTC(), cert.NotAfter.UTC()) - } - return nil -} - -func validateCACertificate(cert *x509.Certificate, expectedPathLen int) error { - if err := validateCABasicConstraints(cert, expectedPathLen); err != nil { - return err - } - return validateCAKeyUsage(cert) -} - -func validateLeafCertificate(cert *x509.Certificate, expectedEku x509.ExtKeyUsage) error { - if err := validateLeafBasicConstraints(cert); err != nil { - return err - } - if err := validateLeafKeyUsage(cert); err != nil { - return err - } - if err := validateExtendedKeyUsage(cert, expectedEku); err != nil { - return err - } - return validateKeyLength(cert) -} - -func validateCABasicConstraints(cert *x509.Certificate, expectedPathLen int) error { - if !cert.BasicConstraintsValid || !cert.IsCA { - return fmt.Errorf("certificate with subject %q: ca field in basic constraints must be present, critical, and set to true", cert.Subject) - } - maxPathLen := cert.MaxPathLen - isMaxPathLenPresent := maxPathLen > 0 || (maxPathLen == 0 && cert.MaxPathLenZero) - if isMaxPathLenPresent && maxPathLen < expectedPathLen { - return fmt.Errorf("certificate with subject %q: expected path length of %d but certificate has path length %d instead", cert.Subject, expectedPathLen, maxPathLen) - } - return nil -} - -func validateLeafBasicConstraints(cert *x509.Certificate) error { - if cert.BasicConstraintsValid && cert.IsCA { - return fmt.Errorf("certificate with subject %q: if the basic constraints extension is present, the ca field must be set to false", cert.Subject) - } - return nil -} - -func validateCAKeyUsage(cert *x509.Certificate) error { - if err := validateKeyUsagePresent(cert); err != nil { - return err - } - if cert.KeyUsage&x509.KeyUsageCertSign == 0 { - return fmt.Errorf("certificate with subject %q: key usage must have the bit positions for key cert sign set", cert.Subject) - } - return nil -} - -func validateLeafKeyUsage(cert *x509.Certificate) error { - if err := validateKeyUsagePresent(cert); err != nil { - return err - } - if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { - return fmt.Errorf("The certificate with subject %q is invalid. The key usage must have the bit positions for \"Digital Signature\" set", cert.Subject) - } - - var invalidKeyUsages []string - if cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"KeyEncipherment"`) - } - if cert.KeyUsage&x509.KeyUsageDataEncipherment != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"DataEncipherment"`) - } - if cert.KeyUsage&x509.KeyUsageKeyAgreement != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"KeyAgreement"`) - } - if cert.KeyUsage&x509.KeyUsageCertSign != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"CertSign"`) - } - if cert.KeyUsage&x509.KeyUsageCRLSign != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"CRLSign"`) - } - if cert.KeyUsage&x509.KeyUsageEncipherOnly != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"EncipherOnly"`) - } - if cert.KeyUsage&x509.KeyUsageDecipherOnly != 0 { - invalidKeyUsages = append(invalidKeyUsages, `"DecipherOnly"`) - } - if len(invalidKeyUsages) > 0 { - return fmt.Errorf("The certificate with subject %q is invalid. The key usage must be \"Digital Signature\" only, but found %s", cert.Subject, strings.Join(invalidKeyUsages, ", ")) - } - return nil -} - -func validateKeyUsagePresent(cert *x509.Certificate) error { - keyUsageExtensionOid := []int{2, 5, 29, 15} - - var hasKeyUsageExtension bool - for _, ext := range cert.Extensions { - if ext.Id.Equal(keyUsageExtensionOid) { - if !ext.Critical { - return fmt.Errorf("certificate with subject %q: key usage extension must be marked critical", cert.Subject) - } - hasKeyUsageExtension = true - break - } - } - if !hasKeyUsageExtension { - return fmt.Errorf("certificate with subject %q: key usage extension must be present", cert.Subject) - } - return nil -} - -func validateExtendedKeyUsage(cert *x509.Certificate, expectedEku x509.ExtKeyUsage) error { - if len(cert.ExtKeyUsage) <= 0 { - return nil - } - - excludedEkus := []x509.ExtKeyUsage{ - x509.ExtKeyUsageAny, - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - x509.ExtKeyUsageEmailProtection, - x509.ExtKeyUsageOCSPSigning, - } - - if expectedEku == 0 { - excludedEkus = append(excludedEkus, x509.ExtKeyUsageTimeStamping) - } else if expectedEku == x509.ExtKeyUsageTimeStamping { - excludedEkus = append(excludedEkus, x509.ExtKeyUsageCodeSigning) - } - - var hasExpectedEku bool - for _, certEku := range cert.ExtKeyUsage { - if certEku == expectedEku { - hasExpectedEku = true - continue - } - for _, excludedEku := range excludedEkus { - if certEku == excludedEku { - return fmt.Errorf("certificate with subject %q: extended key usage must not contain %s eku", cert.Subject, ekuToString(excludedEku)) - } - } - } - - if expectedEku != 0 && !hasExpectedEku { - return fmt.Errorf("certificate with subject %q: extended key usage must contain %s eku", cert.Subject, ekuToString(expectedEku)) - } - return nil -} - -func validateKeyLength(cert *x509.Certificate) error { - switch key := cert.PublicKey.(type) { - case *rsa.PublicKey: - if key.N.BitLen() < 2048 { - return fmt.Errorf("certificate with subject %q: rsa public key length must be 2048 bits or higher", cert.Subject) - } - case *ecdsa.PublicKey: - if key.Params().N.BitLen() < 256 { - return fmt.Errorf("certificate with subject %q: ecdsa public key length must be 256 bits or higher", cert.Subject) - } - } - return nil -} - -func ekuToString(eku x509.ExtKeyUsage) string { - switch eku { - case x509.ExtKeyUsageAny: - return "Any" - case x509.ExtKeyUsageServerAuth: - return "ServerAuth" - case x509.ExtKeyUsageClientAuth: - return "ClientAuth" - case x509.ExtKeyUsageOCSPSigning: - return "OCSPSigning" - case x509.ExtKeyUsageEmailProtection: - return "EmailProtection" - case x509.ExtKeyUsageCodeSigning: - return "CodeSigning" - case x509.ExtKeyUsageTimeStamping: - return "TimeStamping" - default: - return fmt.Sprintf("%d", int(eku)) - } -} diff --git a/x509/codesigning_cert_validations.go b/x509/codesigning_cert_validations.go new file mode 100644 index 00000000..e074cef8 --- /dev/null +++ b/x509/codesigning_cert_validations.go @@ -0,0 +1,173 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package x509 + +import ( + "crypto/x509" + "errors" + "fmt" + "time" + + "github.com/notaryproject/notation-core-go/internal/oid" +) + +// ValidateCodeSigningCertChain takes an ordered code signing certificate chain +// and validates issuance from leaf to root +// Validates certificates according to this spec: +// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#certificate-requirements +func ValidateCodeSigningCertChain(certChain []*x509.Certificate, signingTime *time.Time) error { + if len(certChain) < 1 { + return errors.New("certificate chain must contain at least one certificate") + } + + // For self-signed signing certificate (not a CA) + if len(certChain) == 1 { + cert := certChain[0] + if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil { + return signedTimeError + } + if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { + return fmt.Errorf("invalid self-signed certificate. subject: %q. Error: %w", cert.Subject, err) + } + if err := validateCodeSigningLeafCertificate(cert); err != nil { + return fmt.Errorf("invalid self-signed certificate. Error: %w", err) + } + return nil + } + + for i, cert := range certChain { + if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil { + return signedTimeError + } + if i == len(certChain)-1 { + selfSigned, selfSignedError := isSelfSigned(cert) + if selfSignedError != nil { + return fmt.Errorf("root certificate with subject %q is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: %v", cert.Subject, selfSignedError) + } + if !selfSigned { + return fmt.Errorf("root certificate with subject %q is not self-signed. Certificate chain must end with a valid self-signed root certificate", cert.Subject) + } + } else { + // This is to avoid extra/redundant multiple root cert at the end + // of certificate-chain + selfSigned, selfSignedError := isSelfSigned(cert) + // not checking selfSignedError != nil here because we expect + // a non-nil err. For a non-root certificate, it shouldn't be + // self-signed, hence CheckSignatureFrom would return a non-nil + // error. + if selfSignedError == nil && selfSigned { + if i == 0 { + return fmt.Errorf("leaf certificate with subject %q is self-signed. Certificate chain must not contain self-signed leaf certificate", cert.Subject) + } + return fmt.Errorf("intermediate certificate with subject %q is self-signed. Certificate chain must not contain self-signed intermediate certificate", cert.Subject) + } + parentCert := certChain[i+1] + issuedBy, issuedByError := isIssuedBy(cert, parentCert) + if issuedByError != nil { + return fmt.Errorf("invalid certificates or certificate with subject %q is not issued by %q. Error: %v", cert.Subject, parentCert.Subject, issuedByError) + } + if !issuedBy { + return fmt.Errorf("certificate with subject %q is not issued by %q", cert.Subject, parentCert.Subject) + } + } + + if i == 0 { + if err := validateCodeSigningLeafCertificate(cert); err != nil { + return err + } + } else { + if err := validateCodeSigningCACertificate(cert, i-1); err != nil { + return err + } + } + } + return nil +} + +func validateCodeSigningCACertificate(cert *x509.Certificate, expectedPathLen int) error { + if err := validateCABasicConstraints(cert, expectedPathLen); err != nil { + return err + } + return validateCodeSigningCAKeyUsage(cert) +} + +func validateCodeSigningLeafCertificate(cert *x509.Certificate) error { + if err := validateLeafBasicConstraints(cert); err != nil { + return err + } + if err := validateCodeSigningLeafKeyUsage(cert); err != nil { + return err + } + if err := validateCodeSigningExtendedKeyUsage(cert); err != nil { + return err + } + return validateSignatureAlgorithm(cert) +} + +func validateCodeSigningCAKeyUsage(cert *x509.Certificate) error { + if err := validateCodeSigningKeyUsagePresent(cert); err != nil { + return err + } + if cert.KeyUsage&x509.KeyUsageCertSign == 0 { + return fmt.Errorf("certificate with subject %q: key usage must have the bit positions for key cert sign set", cert.Subject) + } + return nil +} + +func validateCodeSigningLeafKeyUsage(cert *x509.Certificate) error { + if err := validateCodeSigningKeyUsagePresent(cert); err != nil { + return err + } + return validateLeafKeyUsage(cert) +} + +func validateCodeSigningKeyUsagePresent(cert *x509.Certificate) error { + var hasKeyUsageExtension bool + for _, ext := range cert.Extensions { + if ext.Id.Equal(oid.KeyUsage) { + if !ext.Critical { + return fmt.Errorf("certificate with subject %q: key usage extension must be marked critical", cert.Subject) + } + hasKeyUsageExtension = true + break + } + } + if !hasKeyUsageExtension { + return fmt.Errorf("certificate with subject %q: key usage extension must be present", cert.Subject) + } + return nil +} + +func validateCodeSigningExtendedKeyUsage(cert *x509.Certificate) error { + if len(cert.ExtKeyUsage) == 0 { + return nil + } + + excludedEkus := []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + x509.ExtKeyUsageEmailProtection, + x509.ExtKeyUsageTimeStamping, + x509.ExtKeyUsageOCSPSigning, + } + + for _, certEku := range cert.ExtKeyUsage { + for _, excludedEku := range excludedEkus { + if certEku == excludedEku { + return fmt.Errorf("certificate with subject %q: extended key usage must not contain %s eku", cert.Subject, ekuToString(excludedEku)) + } + } + } + return nil +} diff --git a/x509/cert_validations_test.go b/x509/codesigning_cert_validations_test.go similarity index 84% rename from x509/cert_validations_test.go rename to x509/codesigning_cert_validations_test.go index a11289f7..dcb2e230 100644 --- a/x509/cert_validations_test.go +++ b/x509/codesigning_cert_validations_test.go @@ -17,10 +17,12 @@ import ( "crypto/x509" "crypto/x509/pkix" _ "embed" - "encoding/asn1" + "errors" + "os" "testing" "time" + "github.com/notaryproject/notation-core-go/internal/oid" "github.com/notaryproject/notation-core-go/testhelper" ) @@ -100,25 +102,6 @@ var codeSigningLeafPem = "-----BEGIN CERTIFICATE-----\n" + "cwtsQn/iENuvFcfRHcFhvRjEFrIP+Ugx\n" + "-----END CERTIFICATE-----" -var timeStampingLeafPem = "-----BEGIN CERTIFICATE-----\n" + - "MIIC5TCCAc2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1JbnRl\n" + - "cm1lZGlhdGUyMCAXDTIyMDYzMDE5MjAwNFoYDzMwMjExMDMxMTkyMDA0WjAbMRkw\n" + - "FwYDVQQDDBBUaW1lU3RhbXBpbmdMZWFmMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n" + - "MIIBCgKCAQEAyx2ispY5C5sQCiLAuCUTp4wv+fpgHwzE4an8eqi+Jrm0tEabTdzP\n" + - "IdZFRYPZbgRx+D9DKeN76f+rt51G9gOX77fYWyIXgnVL4UAYNlQj58hqZ0IO22vT\n" + - "nIFiDbJoSPuamQaLZNuluiirUwJv1uqSQiEnWHC4LhKwNOo4UHH5S3XkkYRpdFBF\n" + - "Tm4uOTaQJA9dfCh+0wbe7ZlEjDiuk1GTSQu69EPIl4IK7aEWqdvk2z1Pg4YkgJZX\n" + - "mWzkECNayUiBeHj7lL5ZnyZeki2l77WzXe/j5dgQ9E2+63hfBew+O/XeS/Tm/TyQ\n" + - "0P8bQre6vbn9820Cpyg82fd1+5bwYedwVwIDAQABozUwMzAOBgNVHQ8BAf8EBAMC\n" + - "B4AwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDANBgkqhkiG9w0B\n" + - "AQsFAAOCAQEAB9Z80K17p4J3VCqVcKyhgkzzYPoKiBWFThVwxS2+TKY0x4zezSAT\n" + - "69Nmf7NkVH4XyvCEUfgdWYst4t41rH3b5MTMOc5/nPeMccDWT0eZRivodF5hFWZd\n" + - "2QSFiMHmfUhnglY0ocLbfKeI/QoSGiPyBWO0SK6qOszRi14lP0TpgvgNDtMY/Jj5\n" + - "AyINT6o0tyYJvYE23/7ysT3U6pq50M4vOZiSuRys83As/qvlDIDKe8OVlDt6xRvr\n" + - "fqdMFWSk6Iay2OCfYcjUbTutMzSI7dvhDivn5FKnNA6M7QD1lqb7V9fymgrQTsth\n" + - "We9tUxypXgMjYN74QEHYxEAIfNOTeBppWw==\n" + - "-----END CERTIFICATE-----" - var unrelatedCertPem = "-----BEGIN CERTIFICATE-----\n" + "MIIC6jCCAdKgAwIBAgIJAJOlT2AUbsZiMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTcyM1oYDzIxMjIwNjAxMDMxNzIzWjAQMQ4w\n" + @@ -183,7 +166,6 @@ var rootCert = parseCertificateFromString(rootCertPem) var intermediateCert1 = parseCertificateFromString(intermediateCertPem1) var intermediateCert2 = parseCertificateFromString(intermediateCertPem2) var codeSigningCert = parseCertificateFromString(codeSigningLeafPem) -var timeStampingCert = parseCertificateFromString(timeStampingLeafPem) var unrelatedCert = parseCertificateFromString(unrelatedCertPem) var intermediateCertInvalidPathLen = parseCertificateFromString(intermediateCertInvalidPathLenPem) var codeSigningLeafInvalidPathLen = parseCertificateFromString(codeSigningLeafInvalidPathLenPem) @@ -211,15 +193,6 @@ func TestValidCodeSigningChain(t *testing.T) { } } -func TestValidTimeStampingChain(t *testing.T) { - certChain := []*x509.Certificate{timeStampingCert, intermediateCert2, intermediateCert1, rootCert} - signingTime := time.Now() - - if err := ValidateTimeStampingCertChain(certChain, &signingTime); err != nil { - t.Fatal(err) - } -} - func TestFailEmptyChain(t *testing.T) { signingTime := time.Now() err := ValidateCodeSigningCertChain(nil, &signingTime) @@ -340,13 +313,13 @@ func TestInvalidSelfSignedSigningCertificate(t *testing.T) { // ---------------- CA Validations ---------------- func TestValidCa(t *testing.T) { - if err := validateCACertificate(rootCert, 2); err != nil { + if err := validateCodeSigningCACertificate(rootCert, 2); err != nil { t.Fatal(err) } } func TestFailInvalidPathLenCa(t *testing.T) { - err := validateCACertificate(rootCert, 3) + err := validateCodeSigningCACertificate(rootCert, 3) assertErrorEqual("certificate with subject \"CN=Root\": expected path length of 3 but certificate has path length 2 instead", err, t) } @@ -370,7 +343,7 @@ var noBasicConstraintsCaPem = "-----BEGIN CERTIFICATE-----\n" + var noBasicConstraintsCa = parseCertificateFromString(noBasicConstraintsCaPem) func TestFailNoBasicConstraintsCa(t *testing.T) { - err := validateCACertificate(noBasicConstraintsCa, 3) + err := validateCodeSigningCACertificate(noBasicConstraintsCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": ca field in basic constraints must be present, critical, and set to true", err, t) } @@ -394,7 +367,7 @@ var basicConstraintsNotCaPem = "-----BEGIN CERTIFICATE-----\n" + var basicConstraintsNotCa = parseCertificateFromString(basicConstraintsNotCaPem) func TestFailBasicConstraintsNotCa(t *testing.T) { - err := validateCACertificate(basicConstraintsNotCa, 3) + err := validateCodeSigningCACertificate(basicConstraintsNotCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": ca field in basic constraints must be present, critical, and set to true", err, t) } @@ -418,7 +391,7 @@ var kuNotCriticalCertSignCaPem = "-----BEGIN CERTIFICATE-----\n" + var kuNotCriticalCertSignCa = parseCertificateFromString(kuNotCriticalCertSignCaPem) func TestFailKuNotCriticalCertSignCa(t *testing.T) { - err := validateCACertificate(kuNotCriticalCertSignCa, 3) + err := validateCodeSigningCACertificate(kuNotCriticalCertSignCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": key usage extension must be marked critical", err, t) } @@ -442,7 +415,7 @@ var kuMissingCaPem = "-----BEGIN CERTIFICATE-----\n" + var kuMissingCa = parseCertificateFromString(kuMissingCaPem) func TestFailKuMissingCa(t *testing.T) { - err := validateCACertificate(kuMissingCa, 3) + err := validateCodeSigningCACertificate(kuMissingCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": key usage extension must be present", err, t) } @@ -466,11 +439,11 @@ var kuNotCertSignCaPem = "-----BEGIN CERTIFICATE-----\n" + var kuNotCertSignCa = parseCertificateFromString(kuNotCertSignCaPem) func TestFailKuNotCertSignCa(t *testing.T) { - err := validateCACertificate(kuNotCertSignCa, 3) + err := validateCodeSigningCACertificate(kuNotCertSignCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": key usage must have the bit positions for key cert sign set", err, t) } -// ---------------- Code-Signing + Time-Stamping Leaf Validations ---------------- +// ---------------- Code-Signing Validations ---------------- var validNoOptionsLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIICtzCCAZ+gAwIBAgIJAL+FUPhO8J8cMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + @@ -492,7 +465,7 @@ var validNoOptionsLeafPem = "-----BEGIN CERTIFICATE-----\n" + var validNoOptionsLeaf = parseCertificateFromString(validNoOptionsLeafPem) func TestValidNoOptionsLeaf(t *testing.T) { - if err := validateLeafCertificate(validNoOptionsLeaf, x509.ExtKeyUsageCodeSigning); err != nil { + if err := validateCodeSigningLeafCertificate(validNoOptionsLeaf); err != nil { t.Fatal(err) } } @@ -517,7 +490,7 @@ var caTrueLeafPem = "-----BEGIN CERTIFICATE-----\n" + var caTrueLeaf = parseCertificateFromString(caTrueLeafPem) func TestFailCaTrueLeaf(t *testing.T) { - err := validateLeafCertificate(caTrueLeaf, x509.ExtKeyUsageCodeSigning) + err := validateCodeSigningLeafCertificate(caTrueLeaf) assertErrorEqual("certificate with subject \"CN=Hello\": if the basic constraints extension is present, the ca field must be set to false", err, t) } @@ -541,8 +514,8 @@ var kuNoDigitalSignatureLeafPem = "-----BEGIN CERTIFICATE-----\n" + var kuNoDigitalSignatureLeaf = parseCertificateFromString(kuNoDigitalSignatureLeafPem) func TestFailKuNoDigitalSignatureLeaf(t *testing.T) { - err := validateLeafCertificate(kuNoDigitalSignatureLeaf, x509.ExtKeyUsageCodeSigning) - assertErrorEqual("The certificate with subject \"CN=Hello\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", err, t) + err := validateCodeSigningLeafCertificate(kuNoDigitalSignatureLeaf) + assertErrorEqual("the certificate with subject \"CN=Hello\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", err, t) } var kuWrongValuesLeafPem = "-----BEGIN CERTIFICATE-----\n" + @@ -565,8 +538,8 @@ var kuWrongValuesLeafPem = "-----BEGIN CERTIFICATE-----\n" + var kuWrongValuesLeaf = parseCertificateFromString(kuWrongValuesLeafPem) func TestFailKuWrongValuesLeaf(t *testing.T) { - err := validateLeafCertificate(kuWrongValuesLeaf, x509.ExtKeyUsageCodeSigning) - assertErrorEqual("The certificate with subject \"CN=Hello\" is invalid. The key usage must be \"Digital Signature\" only, but found \"CertSign\", \"CRLSign\"", err, t) + err := validateCodeSigningLeafCertificate(kuWrongValuesLeaf) + assertErrorEqual("the certificate with subject \"CN=Hello\" is invalid. The key usage must be \"Digital Signature\" only, but found \"CertSign\", \"CRLSign\"", err, t) } var rsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" + @@ -584,8 +557,8 @@ var rsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" + var rsaKeyTooSmallLeaf = parseCertificateFromString(rsaKeyTooSmallLeafPem) func TestFailRsaKeyTooSmallLeaf(t *testing.T) { - err := validateLeafCertificate(rsaKeyTooSmallLeaf, x509.ExtKeyUsageCodeSigning) - assertErrorEqual("certificate with subject \"CN=Hello\": rsa public key length must be 2048 bits or higher", err, t) + err := validateCodeSigningLeafCertificate(rsaKeyTooSmallLeaf) + assertErrorEqual("certificate with subject \"CN=Hello\": rsa key size 1024 bits is not supported", err, t) } var ecdsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" + @@ -600,141 +573,21 @@ var ecdsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" + var ecdsaKeyTooSmallLeaf = parseCertificateFromString(ecdsaKeyTooSmallLeafPem) func TestFailEcdsaKeyTooSmallLeaf(t *testing.T) { - err := validateLeafCertificate(ecdsaKeyTooSmallLeaf, x509.ExtKeyUsageCodeSigning) - assertErrorEqual("certificate with subject \"CN=Hello\": ecdsa public key length must be 256 bits or higher", err, t) + err := validateCodeSigningLeafCertificate(ecdsaKeyTooSmallLeaf) + assertErrorEqual("certificate with subject \"CN=Hello\": ecdsa key size 224 bits is not supported", err, t) } // ---------------- Code-Signing Leaf Validations ---------------- func TestValidFullOptionsCodeLeaf(t *testing.T) { - if err := validateLeafCertificate(codeSigningCert, x509.ExtKeyUsageCodeSigning); err != nil { + if err := validateCodeSigningLeafCertificate(codeSigningCert); err != nil { t.Fatal(err) } } -var ekuWrongValuesCodeLeafPem = "-----BEGIN CERTIFICATE-----\n" + - "MIIC6jCCAdKgAwIBAgIJAKZJHdWFNYPlMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + - "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMDEwM1oYDzIxMjIwNjAxMDMwMTAzWjAQMQ4w\n" + - "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2t\n" + - "EFpNOJkX7B78d9ahTl5MXGWyKIjgfg1PhkYwHKHJWBiqHa1OUewfUG4ouVuaAvJ+\n" + - "GPzcxt23/J3jK+3/szrzpBNv1f0vgIa+mqaRQDW2m/wfWw3kpcwxlRcL7GnCeHbv\n" + - "gRFDXQW6MhKgGgKdQ5ezV+p01eF+CzMhUe+bZO+mvgxj36MJHzLMFHyh3x4/+z4x\n" + - "qRKmj4uUqJ2FJLlQEk92vPE/N3r7rEWa6gd4mBZ+DsZSrCbVPXchS2mCkeg70qxA\n" + - "4840qVLZ5eFxtqnTEUNytu3ug/8ydV9VmuT+C5fQYUp3Fl7D1QxHxWYTVTKdenCY\n" + - "jxcJHW1cUWZQlgPTLq8CAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgeAMDEGA1UdJQQq\n" + - "MCgGCCsGAQUFBwMDBggrBgEFBQcDAQYIKwYBBQUHAwQGCCsGAQUFBwMIMA0GCSqG\n" + - "SIb3DQEBCwUAA4IBAQBRfpNRu79i47yp73LWTKrnZRiLC4JAI3I3w5TTx8m2tYkq\n" + - "tkSCP3Sn4y6VjKqo9Xtlt/bBLypw7XAOZOUZLEaoCjwRmAwq74VHAxDZO1LfFlKd\n" + - "au8G3xhKjc5prOMJ2g4DELOcyDoLDlwYqQ/jfG/t8b0P37yakFVffSzIA7D0BjmS\n" + - "OnWrGOJO/IJZjiaTdQkg+n5jk4FNqhwW91em64/M3MOmib3plnl89MgR90kuvQOV\n" + - "ctDBylt8M61MgnbzeunAq4aKYJc4IeeIH++g4F3/pqyoC95sAZP+A6+LkmBDOcyE\n" + - "5wUmNtUsL9xxKIUCvPR1JtiLNxHrfendWiuJnW1M\n" + - "-----END CERTIFICATE-----" -var ekuWrongValuesCodeLeaf = parseCertificateFromString(ekuWrongValuesCodeLeafPem) - -func TestFailEkuWrongValuesCodeLeaf(t *testing.T) { - err := validateLeafCertificate(ekuWrongValuesCodeLeaf, x509.ExtKeyUsageCodeSigning) - assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain ServerAuth eku", err, t) -} - -var ekuMissingCodeSigningLeafPem = "-----BEGIN CERTIFICATE-----\n" + - "MIICzDCCAbSgAwIBAgIJAJtYOfTu82KRMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + - "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTMxM1oYDzIxMjIwNjAxMDMxMzEzWjAQMQ4w\n" + - "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALQN\n" + - "GJKHE6cdcmrHkxXOTawWgYEF1X42IOK7gAXFg+KBPHPw4npDjUclLX0sY3XjBuhT\n" + - "wI5DRATSNTV2ba3+DpFuH3D+Hbfjil91AG8XzormUPOOCbZqJxSKYAIZfPQGdUvV\n" + - "UBulnbDsije00HoNZ03IvdjxbB/9y6a3qQEvIUaEjaZBH3s/YYQIiEmKu6eDpj3R\n" + - "PnUcrP5b7jBMA/Vb8joLM0InzqGPRLPFAPf5womAjxZSsrgyVeA1xSm+6KtXMmaA\n" + - "IKYwNVAOnhfqgUk0tlaRyXXji2T1M9w9l5XUA1iNOMcjTUTfFa5KW7c0TLTcK6vW\n" + - "Eq1BEXUEw7HP7DQUjycCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM\n" + - "MAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4IBAQCSr6A/YAMd6lisgipR0UCA\n" + - "4Ye/1kl0jglT7stLTfftSeXgCKXYlwus9VSpZBtg+RvJkihlLNT6vtsiTMfJUBBc\n" + - "jALLKYUQuCw9sReAbfvecIfc2bUve6X8isLWDVnxlC1udx2WG3lIfW2Sgs/dYeZW\n" + - "yqLTagK5GLlDfg9gBpHLmQYOmshhI85ObOioUAiWTW+S6mx4Bphgl7dlcUabJxEJ\n" + - "MpJJiGPkUUUCuYkp31E7S4JRbSXSkaHefZxB5fvhlbnACeqnOtMG/IKaTjCUemkK\n" + - "ZRmJ0Al1PTWs+Dn8zLzexP/LkmQZU/FUMxeat/dAnc2blDbVnAsvcvnutXGHoZH5\n" + - "-----END CERTIFICATE-----" -var ekuMissingCodeSigningLeaf = parseCertificateFromString(ekuMissingCodeSigningLeafPem) - -func TestFailEkuMissingCodeSigningLeaf(t *testing.T) { - err := validateLeafCertificate(ekuMissingCodeSigningLeaf, x509.ExtKeyUsageCodeSigning) - assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain OCSPSigning eku", err, t) -} - -// ---------------- Time-Stamping Leaf Validations ---------------- - -func TestValidFullOptionsTimeLeaf(t *testing.T) { - if err := validateLeafCertificate(timeStampingCert, x509.ExtKeyUsageTimeStamping); err != nil { - t.Fatal(err) - } -} - -var ekuWrongValuesTimeLeafPem = "-----BEGIN CERTIFICATE-----\n" + - "MIIC6jCCAdKgAwIBAgIJAJOlT2AUbsZiMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + - "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTcyM1oYDzIxMjIwNjAxMDMxNzIzWjAQMQ4w\n" + - "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOZe\n" + - "9zjKWNlFD/HGrkaAI9mh9Fw1gF8S2tphQD/aPd9IS4HJJEQRkKz5oeHj2g1Y6TEk\n" + - "plODrKlnoLe+ZFNFFD4xMVV55aQSJDTljCLPwIZt2VewlaAhIImYihOJvJFST1zW\n" + - "K2NW4eLxt0awbE/YzL6beH4A6UsrcXcnN0KKiu6YD1/d5TezJoTQBMo6fboltuce\n" + - "P/+RMxyqpvip7nyFF3Yrmhumb7DKJrmSfSjdziI5QoUqzqVgqJ8pXMRb3ZOKb499\n" + - "d9RRxGkox93iOdSSlaP3FEl8VK9KqnD+MNhjVZbeYTfjm9UVdp91VLP1E/yfMXz+\n" + - "fZhYkublK6v3GWSEcb0CAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgeAMDEGA1UdJQQq\n" + - "MCgGCCsGAQUFBwMIBggrBgEFBQcDAQYIKwYBBQUHAwQGCCsGAQUFBwMIMA0GCSqG\n" + - "SIb3DQEBCwUAA4IBAQCaQZ+ws93F1azT6SKBYvBRBCj07+2DtNI83Q53GxrVy2vU\n" + - "rP1ULX7beY87amy6kQcqnQ0QSaoLK+CDL88pPxR2PBzCauz70rMRY8O/KrrLcfwd\n" + - "D5HM9DcbneqXQyfh0ZQpt0wK5wux0MFh2sAEv76jgYBMHq2zc+19skAW/oBtTUty\n" + - "i/IdOVeO589KXwJzEJmKiswN9zKo9KGgAlKS05zohjv40AOCAs+8Q2lOJjRMq4Ji\n" + - "z21qor5e/5+NnGY+2p4A7PbN+QnDdRC3y16dESRN50o5x6CwUWQO74+uRjrAWYCm\n" + - "f/Y7qdOf5zZbY21n8KnLcFOsKhwv4t40Y/LQqN/L\n" + - "-----END CERTIFICATE-----" -var ekuWrongValuesTimeLeaf = parseCertificateFromString(ekuWrongValuesTimeLeafPem) - -func TestFailEkuWrongValuesTimeLeaf(t *testing.T) { - err := validateLeafCertificate(ekuWrongValuesTimeLeaf, x509.ExtKeyUsageTimeStamping) - assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain ServerAuth eku", err, t) -} - -var ekuMissingTimeStampingLeafPem = "-----BEGIN CERTIFICATE-----\n" + - "MIICzDCCAbSgAwIBAgIJAJtYOfTu82KRMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + - "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTMxM1oYDzIxMjIwNjAxMDMxMzEzWjAQMQ4w\n" + - "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALQN\n" + - "GJKHE6cdcmrHkxXOTawWgYEF1X42IOK7gAXFg+KBPHPw4npDjUclLX0sY3XjBuhT\n" + - "wI5DRATSNTV2ba3+DpFuH3D+Hbfjil91AG8XzormUPOOCbZqJxSKYAIZfPQGdUvV\n" + - "UBulnbDsije00HoNZ03IvdjxbB/9y6a3qQEvIUaEjaZBH3s/YYQIiEmKu6eDpj3R\n" + - "PnUcrP5b7jBMA/Vb8joLM0InzqGPRLPFAPf5womAjxZSsrgyVeA1xSm+6KtXMmaA\n" + - "IKYwNVAOnhfqgUk0tlaRyXXji2T1M9w9l5XUA1iNOMcjTUTfFa5KW7c0TLTcK6vW\n" + - "Eq1BEXUEw7HP7DQUjycCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM\n" + - "MAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4IBAQCSr6A/YAMd6lisgipR0UCA\n" + - "4Ye/1kl0jglT7stLTfftSeXgCKXYlwus9VSpZBtg+RvJkihlLNT6vtsiTMfJUBBc\n" + - "jALLKYUQuCw9sReAbfvecIfc2bUve6X8isLWDVnxlC1udx2WG3lIfW2Sgs/dYeZW\n" + - "yqLTagK5GLlDfg9gBpHLmQYOmshhI85ObOioUAiWTW+S6mx4Bphgl7dlcUabJxEJ\n" + - "MpJJiGPkUUUCuYkp31E7S4JRbSXSkaHefZxB5fvhlbnACeqnOtMG/IKaTjCUemkK\n" + - "ZRmJ0Al1PTWs+Dn8zLzexP/LkmQZU/FUMxeat/dAnc2blDbVnAsvcvnutXGHoZH5\n" + - "-----END CERTIFICATE-----" -var ekuMissingTimeStampingLeaf = parseCertificateFromString(ekuMissingTimeStampingLeafPem) - -func TestFailEkuMissingTimeStampingLeaf(t *testing.T) { - err := validateLeafCertificate(ekuMissingTimeStampingLeaf, x509.ExtKeyUsageTimeStamping) - assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain OCSPSigning eku", err, t) -} - -// ---------------- Utility Methods ---------------- - -func parseCertificateFromString(certPem string) *x509.Certificate { - stringAsBytes := []byte(certPem) - cert, _ := parseCertificates(stringAsBytes) - return cert[0] -} - -func assertErrorEqual(expected string, err error, t *testing.T) { - if err == nil || expected != err.Error() { - t.Fatalf("Expected error \"%v\" but was \"%v\"", expected, err) - } -} - func TestValidateLeafKeyUsage(t *testing.T) { extensions := []pkix.Extension{{ - Id: asn1.ObjectIdentifier{2, 5, 29, 15}, // OID for KeyUsage + Id: oid.KeyUsage, Critical: true, }} @@ -768,7 +621,7 @@ func TestValidateLeafKeyUsage(t *testing.T) { KeyUsage: x509.KeyUsageCertSign, Extensions: extensions, }, - expectedErrMsg: "The certificate with subject \"CN=Test CN\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", + expectedErrMsg: "the certificate with subject \"CN=Test CN\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", }, { name: "Invalid KeyEncipherment usage", @@ -777,7 +630,7 @@ func TestValidateLeafKeyUsage(t *testing.T) { KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, Extensions: extensions, }, - expectedErrMsg: "The certificate with subject \"CN=Test CN\" is invalid. The key usage must be \"Digital Signature\" only, but found \"KeyEncipherment\"", + expectedErrMsg: "the certificate with subject \"CN=Test CN\" is invalid. The key usage must be \"Digital Signature\" only, but found \"KeyEncipherment\"", }, { name: "Multiple Invalid usages", @@ -786,7 +639,7 @@ func TestValidateLeafKeyUsage(t *testing.T) { KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageEncipherOnly | x509.KeyUsageDecipherOnly | x509.KeyUsageEncipherOnly | x509.KeyUsageDecipherOnly, Extensions: extensions, }, - expectedErrMsg: "The certificate with subject \"CN=Test CN\" is invalid. The key usage must be \"Digital Signature\" only, but found \"KeyEncipherment\", \"DataEncipherment\", \"KeyAgreement\", \"CertSign\", \"CRLSign\", \"EncipherOnly\", \"DecipherOnly\"", + expectedErrMsg: "the certificate with subject \"CN=Test CN\" is invalid. The key usage must be \"Digital Signature\" only, but found \"KeyEncipherment\", \"DataEncipherment\", \"KeyAgreement\", \"CertSign\", \"CRLSign\", \"EncipherOnly\", \"DecipherOnly\"", }, } @@ -803,3 +656,81 @@ func TestValidateLeafKeyUsage(t *testing.T) { }) } } + +var ekuWrongValuesCodeLeafPem = "-----BEGIN CERTIFICATE-----\n" + + "MIIC6jCCAdKgAwIBAgIJAKZJHdWFNYPlMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMDEwM1oYDzIxMjIwNjAxMDMwMTAzWjAQMQ4w\n" + + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2t\n" + + "EFpNOJkX7B78d9ahTl5MXGWyKIjgfg1PhkYwHKHJWBiqHa1OUewfUG4ouVuaAvJ+\n" + + "GPzcxt23/J3jK+3/szrzpBNv1f0vgIa+mqaRQDW2m/wfWw3kpcwxlRcL7GnCeHbv\n" + + "gRFDXQW6MhKgGgKdQ5ezV+p01eF+CzMhUe+bZO+mvgxj36MJHzLMFHyh3x4/+z4x\n" + + "qRKmj4uUqJ2FJLlQEk92vPE/N3r7rEWa6gd4mBZ+DsZSrCbVPXchS2mCkeg70qxA\n" + + "4840qVLZ5eFxtqnTEUNytu3ug/8ydV9VmuT+C5fQYUp3Fl7D1QxHxWYTVTKdenCY\n" + + "jxcJHW1cUWZQlgPTLq8CAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgeAMDEGA1UdJQQq\n" + + "MCgGCCsGAQUFBwMDBggrBgEFBQcDAQYIKwYBBQUHAwQGCCsGAQUFBwMIMA0GCSqG\n" + + "SIb3DQEBCwUAA4IBAQBRfpNRu79i47yp73LWTKrnZRiLC4JAI3I3w5TTx8m2tYkq\n" + + "tkSCP3Sn4y6VjKqo9Xtlt/bBLypw7XAOZOUZLEaoCjwRmAwq74VHAxDZO1LfFlKd\n" + + "au8G3xhKjc5prOMJ2g4DELOcyDoLDlwYqQ/jfG/t8b0P37yakFVffSzIA7D0BjmS\n" + + "OnWrGOJO/IJZjiaTdQkg+n5jk4FNqhwW91em64/M3MOmib3plnl89MgR90kuvQOV\n" + + "ctDBylt8M61MgnbzeunAq4aKYJc4IeeIH++g4F3/pqyoC95sAZP+A6+LkmBDOcyE\n" + + "5wUmNtUsL9xxKIUCvPR1JtiLNxHrfendWiuJnW1M\n" + + "-----END CERTIFICATE-----" +var ekuWrongValuesCodeLeaf = parseCertificateFromString(ekuWrongValuesCodeLeafPem) + +func TestFailEkuWrongValuesCodeLeaf(t *testing.T) { + err := validateCodeSigningLeafCertificate(ekuWrongValuesCodeLeaf) + assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain ServerAuth eku", err, t) +} + +var ekuMissingCodeSigningLeafPem = "-----BEGIN CERTIFICATE-----\n" + + "MIICzDCCAbSgAwIBAgIJAJtYOfTu82KRMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTMxM1oYDzIxMjIwNjAxMDMxMzEzWjAQMQ4w\n" + + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALQN\n" + + "GJKHE6cdcmrHkxXOTawWgYEF1X42IOK7gAXFg+KBPHPw4npDjUclLX0sY3XjBuhT\n" + + "wI5DRATSNTV2ba3+DpFuH3D+Hbfjil91AG8XzormUPOOCbZqJxSKYAIZfPQGdUvV\n" + + "UBulnbDsije00HoNZ03IvdjxbB/9y6a3qQEvIUaEjaZBH3s/YYQIiEmKu6eDpj3R\n" + + "PnUcrP5b7jBMA/Vb8joLM0InzqGPRLPFAPf5womAjxZSsrgyVeA1xSm+6KtXMmaA\n" + + "IKYwNVAOnhfqgUk0tlaRyXXji2T1M9w9l5XUA1iNOMcjTUTfFa5KW7c0TLTcK6vW\n" + + "Eq1BEXUEw7HP7DQUjycCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM\n" + + "MAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4IBAQCSr6A/YAMd6lisgipR0UCA\n" + + "4Ye/1kl0jglT7stLTfftSeXgCKXYlwus9VSpZBtg+RvJkihlLNT6vtsiTMfJUBBc\n" + + "jALLKYUQuCw9sReAbfvecIfc2bUve6X8isLWDVnxlC1udx2WG3lIfW2Sgs/dYeZW\n" + + "yqLTagK5GLlDfg9gBpHLmQYOmshhI85ObOioUAiWTW+S6mx4Bphgl7dlcUabJxEJ\n" + + "MpJJiGPkUUUCuYkp31E7S4JRbSXSkaHefZxB5fvhlbnACeqnOtMG/IKaTjCUemkK\n" + + "ZRmJ0Al1PTWs+Dn8zLzexP/LkmQZU/FUMxeat/dAnc2blDbVnAsvcvnutXGHoZH5\n" + + "-----END CERTIFICATE-----" +var ekuMissingCodeSigningLeaf = parseCertificateFromString(ekuMissingCodeSigningLeafPem) + +func TestFailEkuMissingCodeSigningLeaf(t *testing.T) { + err := validateCodeSigningLeafCertificate(ekuMissingCodeSigningLeaf) + assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain OCSPSigning eku", err, t) +} + +// ---------------- Utility Methods ---------------- + +func parseCertificateFromString(certPem string) *x509.Certificate { + stringAsBytes := []byte(certPem) + cert, _ := parseCertificates(stringAsBytes) + return cert[0] +} + +func assertErrorEqual(expected string, err error, t *testing.T) { + if err == nil || expected != err.Error() { + t.Fatalf("Expected error \"%v\" but was \"%v\"", expected, err) + } +} + +func readSingleCertificate(path string) (*x509.Certificate, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + certs, err := parseCertificates(data) + if err != nil { + return nil, err + } + if len(certs) == 0 { + return nil, errors.New("no certificate in file") + } + return certs[0], nil +} diff --git a/x509/helper.go b/x509/helper.go new file mode 100644 index 00000000..91d34430 --- /dev/null +++ b/x509/helper.go @@ -0,0 +1,127 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package x509 + +import ( + "bytes" + "crypto/x509" + "fmt" + "strings" + "time" + + "github.com/notaryproject/notation-core-go/signature" +) + +func isSelfSigned(cert *x509.Certificate) (bool, error) { + return isIssuedBy(cert, cert) +} + +func isIssuedBy(subject *x509.Certificate, issuer *x509.Certificate) (bool, error) { + if err := subject.CheckSignatureFrom(issuer); err != nil { + return false, err + } + return bytes.Equal(issuer.RawSubject, subject.RawIssuer), nil +} + +func validateSigningTime(cert *x509.Certificate, signingTime *time.Time) error { + if signingTime != nil && (signingTime.Before(cert.NotBefore) || signingTime.After(cert.NotAfter)) { + return fmt.Errorf("certificate with subject %q was invalid at signing time of %s. Certificate is valid from [%s] to [%s]", + cert.Subject, signingTime.UTC(), cert.NotBefore.UTC(), cert.NotAfter.UTC()) + } + return nil +} + +func validateCABasicConstraints(cert *x509.Certificate, expectedPathLen int) error { + if !cert.BasicConstraintsValid || !cert.IsCA { + return fmt.Errorf("certificate with subject %q: ca field in basic constraints must be present, critical, and set to true", cert.Subject) + } + maxPathLen := cert.MaxPathLen + isMaxPathLenPresent := maxPathLen > 0 || (maxPathLen == 0 && cert.MaxPathLenZero) + if isMaxPathLenPresent && maxPathLen < expectedPathLen { + return fmt.Errorf("certificate with subject %q: expected path length of %d but certificate has path length %d instead", cert.Subject, expectedPathLen, maxPathLen) + } + return nil +} + +func validateLeafBasicConstraints(cert *x509.Certificate) error { + if cert.BasicConstraintsValid && cert.IsCA { + return fmt.Errorf("certificate with subject %q: if the basic constraints extension is present, the ca field must be set to false", cert.Subject) + } + return nil +} + +func validateLeafKeyUsage(cert *x509.Certificate) error { + if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { + return fmt.Errorf("the certificate with subject %q is invalid. The key usage must have the bit positions for \"Digital Signature\" set", cert.Subject) + } + + var invalidKeyUsages []string + if cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"KeyEncipherment"`) + } + if cert.KeyUsage&x509.KeyUsageDataEncipherment != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"DataEncipherment"`) + } + if cert.KeyUsage&x509.KeyUsageKeyAgreement != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"KeyAgreement"`) + } + if cert.KeyUsage&x509.KeyUsageCertSign != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"CertSign"`) + } + if cert.KeyUsage&x509.KeyUsageCRLSign != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"CRLSign"`) + } + if cert.KeyUsage&x509.KeyUsageEncipherOnly != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"EncipherOnly"`) + } + if cert.KeyUsage&x509.KeyUsageDecipherOnly != 0 { + invalidKeyUsages = append(invalidKeyUsages, `"DecipherOnly"`) + } + if len(invalidKeyUsages) > 0 { + return fmt.Errorf("the certificate with subject %q is invalid. The key usage must be \"Digital Signature\" only, but found %s", cert.Subject, strings.Join(invalidKeyUsages, ", ")) + } + return nil +} + +func validateSignatureAlgorithm(cert *x509.Certificate) error { + keySpec, err := signature.ExtractKeySpec(cert) + if err != nil { + return fmt.Errorf("certificate with subject %q: %w", cert.Subject, err) + } + if keySpec.SignatureAlgorithm() == 0 { + return fmt.Errorf("certificate with subject %q: unsupported signature algorithm with key spec %+v", cert.Subject, keySpec) + } + return nil +} + +func ekuToString(eku x509.ExtKeyUsage) string { + switch eku { + case x509.ExtKeyUsageAny: + return "Any" + case x509.ExtKeyUsageServerAuth: + return "ServerAuth" + case x509.ExtKeyUsageClientAuth: + return "ClientAuth" + case x509.ExtKeyUsageOCSPSigning: + return "OCSPSigning" + case x509.ExtKeyUsageEmailProtection: + return "EmailProtection" + case x509.ExtKeyUsageCodeSigning: + return "CodeSigning" + case x509.ExtKeyUsageTimeStamping: + return "Timestamping" + default: + return fmt.Sprintf("%d", int(eku)) + } +} diff --git a/x509/testdata/timestamp_intermediate.crt b/x509/testdata/timestamp_intermediate.crt new file mode 100644 index 0000000000000000000000000000000000000000..7c375380bc83af86d6916a32fdb2117b637db5ac GIT binary patch literal 1714 zcmXqLVq0g>#4>FGGZP~dlK{J!`F53%nj7wG{%WLY#YG$NvTYLy~7g4A9B^{qLe${@S)5%>wSAHJ9v3!6q zw_5SE+#lg9m}>tzZ)v$D__287=6Bi+EnP|y_g~+#JtAJeT2kB2=}vmzt3cZi*TSwm z*U)`^dE?g7=kjc+y;C$V-+YsK|K@~tH-_rlE{>gdYO=zsKA2a|k6m;6MN7(-pigD) z-xHf9@5XNl;a1GZGgsX0uqJ-fJ+InxUoC~w-}5a^&Uv!zsO(uY-k7V;q*okFDREeS zpy=o>KlzPYBYZ3WUGFcRyhg|J(Nd3VA2t{YuMF3mrmpw3eObR>nP{k5!3?$f1yb$` z&X1jw8>&n5)Kw?Uu(A-`{O#MkXV%eg8t*BI+-%Z1K574gdynlF75{oJAC)uZQ_xgF z?T8Wwjn&1@=k9wgl*zU@s`>fUs%gbe8S}TfZJukj_$u>*{T~+DoN~G&;ZZMQa{iLf z>i4s4|MBfe(fU=Yq2l<~X+P_nG|!-bFM*%UJbk+rF6o4uR%);K?3%Dgxi{kboy2>S z|5+aRxyfLu&-Irxydvx_Gk5&k+^2r_^>*y|}6$jNgeu@YPG1tZ`?@oD~ z{9xHbyKjd|HrdN@mc3aW>sIWR#Q?Sn2EMCl?o> zWI_W+xGJVLQ>d!sL{J_rMp38_%Q(<{1InT9CVI(5B?itQ*U7Wk7+4utEHGPO(q;rR zrzppmI74y_6hMY3umFn-0}eK}^hO>=7A6C3HqL}L55~5?IqZzcSrAzK0J9(?Lv3W= zfyvwQXRLHlVysKMc0`75;&zZD$Zq0=PuA28N_LTbX ziu%4%U{mbN{~95kAFg&4mZjc0^Py<5@~;ExO)l$Zs!Oj{kyAUk{hrqAZ}T=*&TL#9 z*0!3<_2m5{y$hbt`2Bm?+@keIjvqXjRe1B5a0BO&ZS&8zx2x>-U+lTFJZ<8~qB$?U znSz#^*G&#yV=c?!$v@?+W!2sPs>w6eb!_dkmw!wlulfp^Y)v}*h6t7;!=)c$Ax?-)*+TTZ}R=?$nH@0kdtf;bhIp^VB<-f8TmyiE^ z@o`4{Z|xg(pWNn_>d2|nB+pVsO&M@4P#L3UKxACU%zx_)teh$C6hviq(J)`?9ogWy^ z#vb~4LAjoR`$-v-+RYECWlqa9+d18}pVdn(Y32VjW7@aS5N-K}Uk5(%X+OR8d!@dM WsMm90KH1ym7E%kfzrDRy^9KNDimN>U literal 0 HcmV?d00001 diff --git a/x509/testdata/timestamp_leaf.crt b/x509/testdata/timestamp_leaf.crt new file mode 100644 index 0000000000000000000000000000000000000000..fdc6e067b69a7317e225ecf40e52189d371054ad GIT binary patch literal 1734 zcmb7^dpy%?9LM+DW-hsvSjuHCmD{mrGuM^Vm|Kxsw4>xUF{Y-OODHC*93>?rYK~NN z(S;=`5l2mu=3W+!blgjKC(d@tIrZN;zyChp@8|dWe4f|qc|U-SY5;6xITrzk;BaLr za>Yzek&N}(x~D$}3j%0fH7u!9WOZ5s*b8e2^#=xQ{mu4OAsUPDPPS z3!~YG?us$NxaX(jJGohZ*GCvM2iab zjtPol&~}7j>`6cit$;TJmN)|V`}0F9m=SOQXNEU3w>G!-1J3`k1O{k*ErI^GggA;R ziNfH4ID-w>{r-fI6b!+JE5XEbIXD{*!AiUx_G_jc-g41)({;Upw$t6iPe%NU6=k1c zolEj|6O9VPo&~E?>dTHTKG#})Euvr9FheUigeIUAnn%~!`lZ+LDyQyv zL+i+a_+RowWaIUn#~)3}phwwMw|PAgpEFr`Mu*B6>m26xgv!U(+-3O->N08h*ysq>Qx$`kY;^fh{Pg>CiD+|EH|mAN1%xy-p~UUO%&kgh*n zXUFDqqt_N5J%+5LF;*+M->9-^e-I#ZhEXyu_j<7@b)ZGpnE%>9MaRW+4{E!Bc<1w2 zwpEX(G(YFY3y9BGGUF z18NerQljL697@I*1xZOsBV+(ZGRGj*0UF4lP+_U^kl4NmB^$_bhB#m7(h$&=IP{Rq zfLhAZ8nxaiw{$GiFtz~8%8bBdow~mhXi5xb^C9Y)bNcocP)#)>bwT|{Rtg|}pf1hi* zGJa)|k<{*ehWWhB30Y59`5IrDId|Jqz9&D{-S?(f1Fy)cEOjJ`v^A5~e$%+|_r$is z&+#vgEsy<;#u9i8k(b=TD4Z%IKmIuE5+~!Q(^T1(e|-6-nwz!tH1(P`wKr*&gdJB% ztNMk0dG2P1Iu6ApvmA;`DXSt74#HlKDv!(x-`lQDgN{uGf=eUzFoy79{wei?h|!L0 z%PBXnnFoP7jlND3eTz3_;_n@JNqND0YWpeRid|Qh2%X(te%D9Dsk^w^g!H(;Z2ftQ zj+?u3_v9s5IS23?o~^|{XLVVG`5pZD{#^60yZIW}?K_1=^aSWxANi$?(26L%&6@Hy ziHY;!rYvq*D!=D)?cABMgwXQvE?quxtblF4*+8v@_6A>Y?MaVK^;JhsWDWXDV;o(T ze)&QL)8>_nMj#fDdd9yk#(NHSFcrbrG$r zxAE<`06pswIWHZj9i2xeJ*ajR(D6wWY?xE%@CqjH#HUCTl;adhYkE3h;A!urBPryH I6lGGzU-iDo4*&oF literal 0 HcmV?d00001 diff --git a/x509/testdata/timestamp_root.crt b/x509/testdata/timestamp_root.crt new file mode 100644 index 0000000000000000000000000000000000000000..99bcc84b7e68b5b28e4444f6fa21bc7c2baf497d GIT binary patch literal 1428 zcmXqLVx3^n#9Xm}nTe5!Nq}{>bojhJMWaWS?0c7&m&O?IvTn?JkswJuAWqo zP1e*s_a@Ho#N;1}iL*^!vmT3k6D_sp^~v*R*O)lOZ>&mtSAN1{MOt|H{E&z~9_{V^ z%MEUZy*pJM`*`h1|G1~7&kaxCnjCkhufO5ewuv(wCR84-IKFM;k*!%07R&;@H?Ej3 z(PORc_}XMAFtK2DXp^JS_1i4PT6q&0YZQI1>{%zxTpC-EcGJqxWtOqSeva!=o=Xlr zTe%?p?h^Gq3;iv(3Py;3SBY`!Px*c@v!iTAnQdgOQ(1fG^vo)c4-XazNvF*!Id#ul z?m1ubx@TA3Pnu*k&-M<(6Ia#FZL?e?wd)Q{*>Wi{_qFlOqxZd87|ztnOg-HHU2)SU z!R@>2KV9u9&~Z#ywJ}-3WvWzJQr)+P4ZmNcEHl2?$^LNf_GivZBz7z-XMD&%g-20# zQ;4Q&XUw*5U*l71pZrEK z{)j?gcK*iIZQcHduDQm~Rrs?|?&yL3MH}n5)MkEtlBqvKR`=`8m78RrN;5GtGB7T7 zGH@{92PS7(VMfOPEUX61K+1p*B)|_6U;*Z-HUn7@pN~b1MdZ!($4!?CV^e(Y>!sU2 z-!)^M48K2eDg$OPU@Bu|*qwN@c4f{!@gozZ4=-HA(EB(ggFozi`MQFie`k5k+~v@ z;|G_o$XKF&XYNn+bq1|Fzoq+H+4VTN((ZIU8a!-?Xwiwjs2;$JM? zvV8rD@42RPYNEQXEwY&TxuW}v?-ZoX1m*e!D1M{-ku zW1+3RZ-H_fkJp{XOJ|IxwD59pPM7gN@Ge`S#Ng5cOA~=sMNvkM7okS?3O#RXhzIyS z+vj_+bj^iRza4itFI{!{FsqBdj@j%-zaF{nP!7&v%TEujciZY?pQjO3sdj0}ilph6 zfR&$*WHWvetKHnrfA0t)=$1ze_=^}`TkG{L*Rlgt&`^}&Rl)f&LXk-+o%1iN!lfsC)s~w6 z?Tp>inj~vqxWQ7QGU@KxG3!Bhh+^5 ouE}lQ!_OaFs=4ZwaQTyaJ&lTM*#+DM*S6cTUo72o{&QL#0GZBIfB*mh literal 0 HcmV?d00001 diff --git a/x509/timestamp_cert_validations.go b/x509/timestamp_cert_validations.go new file mode 100644 index 00000000..adb46e60 --- /dev/null +++ b/x509/timestamp_cert_validations.go @@ -0,0 +1,161 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package x509 + +import ( + "crypto/x509" + "errors" + "fmt" + + "github.com/notaryproject/notation-core-go/internal/oid" +) + +// ValidateTimestampingCertChain takes an ordered time stamping certificate +// chain and validates issuance from leaf to root +// Validates certificates according to this spec: +// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#certificate-requirements +func ValidateTimestampingCertChain(certChain []*x509.Certificate) error { + if len(certChain) < 1 { + return errors.New("certificate chain must contain at least one certificate") + } + + // For self-signed signing certificate (not a CA) + if len(certChain) == 1 { + cert := certChain[0] + if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { + return fmt.Errorf("invalid self-signed certificate. subject: %q. Error: %w", cert.Subject, err) + } + if err := validateTimestampingLeafCertificate(cert); err != nil { + return fmt.Errorf("invalid self-signed certificate. Error: %w", err) + } + return nil + } + + for i, cert := range certChain { + if i == len(certChain)-1 { + selfSigned, selfSignedError := isSelfSigned(cert) + if selfSignedError != nil { + return fmt.Errorf("root certificate with subject %q is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: %v", cert.Subject, selfSignedError) + } + if !selfSigned { + return fmt.Errorf("root certificate with subject %q is not self-signed. Certificate chain must end with a valid self-signed root certificate", cert.Subject) + } + } else { + // This is to avoid extra/redundant multiple root cert at the end + // of certificate-chain + selfSigned, selfSignedError := isSelfSigned(cert) + // not checking selfSignedError != nil here because we expect + // a non-nil err. For a non-root certificate, it shouldn't be + // self-signed, hence CheckSignatureFrom would return a non-nil + // error. + if selfSignedError == nil && selfSigned { + if i == 0 { + return fmt.Errorf("leaf certificate with subject %q is self-signed. Certificate chain must not contain self-signed leaf certificate", cert.Subject) + } + return fmt.Errorf("intermediate certificate with subject %q is self-signed. Certificate chain must not contain self-signed intermediate certificate", cert.Subject) + } + parentCert := certChain[i+1] + issuedBy, issuedByError := isIssuedBy(cert, parentCert) + if issuedByError != nil { + return fmt.Errorf("invalid certificates or certificate with subject %q is not issued by %q. Error: %v", cert.Subject, parentCert.Subject, issuedByError) + } + if !issuedBy { + return fmt.Errorf("certificate with subject %q is not issued by %q", cert.Subject, parentCert.Subject) + } + } + + if i == 0 { + if err := validateTimestampingLeafCertificate(cert); err != nil { + return err + } + } else { + if err := validateTimestampingCACertificate(cert, i-1); err != nil { + return err + } + } + } + return nil +} + +func validateTimestampingCACertificate(cert *x509.Certificate, expectedPathLen int) error { + if err := validateCABasicConstraints(cert, expectedPathLen); err != nil { + return err + } + return validateTimestampingCAKeyUsage(cert) +} + +func validateTimestampingLeafCertificate(cert *x509.Certificate) error { + if err := validateLeafBasicConstraints(cert); err != nil { + return err + } + if err := validateTimestampingLeafKeyUsage(cert); err != nil { + return err + } + if err := validateTimestampingExtendedKeyUsage(cert); err != nil { + return err + } + return validateSignatureAlgorithm(cert) +} + +func validateTimestampingCAKeyUsage(cert *x509.Certificate) error { + if err := validateTimestampingKeyUsagePresent(cert); err != nil { + return err + } + if cert.KeyUsage&x509.KeyUsageCertSign == 0 { + return fmt.Errorf("certificate with subject %q: key usage must have the bit positions for key cert sign set", cert.Subject) + } + return nil +} + +func validateTimestampingLeafKeyUsage(cert *x509.Certificate) error { + if err := validateTimestampingKeyUsagePresent(cert); err != nil { + return err + } + return validateLeafKeyUsage(cert) +} + +func validateTimestampingKeyUsagePresent(cert *x509.Certificate) error { + var hasKeyUsageExtension bool + for _, ext := range cert.Extensions { + if ext.Id.Equal(oid.KeyUsage) { + hasKeyUsageExtension = true + break + } + } + if !hasKeyUsageExtension { + return fmt.Errorf("certificate with subject %q: key usage extension must be present", cert.Subject) + } + return nil +} + +func validateTimestampingExtendedKeyUsage(cert *x509.Certificate) error { + // RFC 3161 2.3: The corresponding certificate MUST contain only one + // instance of the extended key usage field extension. And it MUST be + // marked as critical. + if len(cert.ExtKeyUsage) != 1 || + cert.ExtKeyUsage[0] != x509.ExtKeyUsageTimeStamping || + len(cert.UnknownExtKeyUsage) != 0 { + return fmt.Errorf("timestamp signing certificate with subject %q must have and only have %s as extended key usage", cert.Subject, ekuToString(x509.ExtKeyUsageTimeStamping)) + } + // check if Extended Key Usage extension is marked critical + for _, ext := range cert.Extensions { + if ext.Id.Equal(oid.ExtKeyUsage) { + if !ext.Critical { + return fmt.Errorf("timestamp signing certificate with subject %q must have extended key usage extension marked as critical", cert.Subject) + } + break + } + } + return nil +} diff --git a/x509/timestamp_cert_validations_test.go b/x509/timestamp_cert_validations_test.go new file mode 100644 index 00000000..cfede2da --- /dev/null +++ b/x509/timestamp_cert_validations_test.go @@ -0,0 +1,217 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package x509 + +import ( + "crypto/x509" + "crypto/x509/pkix" + "testing" +) + +func TestValidTimestampingChain(t *testing.T) { + timestamp_leaf, err := readSingleCertificate("testdata/timestamp_leaf.crt") + if err != nil { + t.Fatal(err) + } + timestamp_intermediate, err := readSingleCertificate("testdata/timestamp_intermediate.crt") + if err != nil { + t.Fatal(err) + } + timestamp_root, err := readSingleCertificate("testdata/timestamp_root.crt") + if err != nil { + t.Fatal(err) + } + certChain := []*x509.Certificate{timestamp_leaf, timestamp_intermediate, timestamp_root} + err = ValidateTimestampingCertChain(certChain) + if err != nil { + t.Fatal(err) + } +} + +func TestInvalidTimestampingChain(t *testing.T) { + timestamp_leaf, err := readSingleCertificate("testdata/timestamp_leaf.crt") + if err != nil { + t.Fatal(err) + } + timestamp_intermediate, err := readSingleCertificate("testdata/timestamp_intermediate.crt") + if err != nil { + t.Fatal(err) + } + timestamp_root, err := readSingleCertificate("testdata/timestamp_root.crt") + if err != nil { + t.Fatal(err) + } + + expectedErr := "certificate chain must contain at least one certificate" + err = ValidateTimestampingCertChain([]*x509.Certificate{}) + assertErrorEqual(expectedErr, err, t) + + certChain := []*x509.Certificate{timestamp_leaf, intermediateCert2, intermediateCert1, rootCert} + expectedErr = "invalid certificates or certificate with subject \"CN=DigiCert Timestamp 2023,O=DigiCert\\\\, Inc.,C=US\" is not issued by \"CN=Intermediate2\". Error: crypto/rsa: verification error" + err = ValidateTimestampingCertChain(certChain) + assertErrorEqual(expectedErr, err, t) + + certChain = []*x509.Certificate{timestamp_leaf} + expectedErr = "invalid self-signed certificate. subject: \"CN=DigiCert Timestamp 2023,O=DigiCert\\\\, Inc.,C=US\". Error: crypto/rsa: verification error" + err = ValidateTimestampingCertChain(certChain) + assertErrorEqual(expectedErr, err, t) + + certChain = []*x509.Certificate{timestamp_leaf, timestamp_intermediate} + expectedErr = "root certificate with subject \"CN=DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA,O=DigiCert\\\\, Inc.,C=US\" is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: crypto/rsa: verification error" + err = ValidateTimestampingCertChain(certChain) + assertErrorEqual(expectedErr, err, t) + + certChain = []*x509.Certificate{timestamp_root, timestamp_root} + expectedErr = "leaf certificate with subject \"CN=DigiCert Trusted Root G4,OU=www.digicert.com,O=DigiCert Inc,C=US\" is self-signed. Certificate chain must not contain self-signed leaf certificate" + err = ValidateTimestampingCertChain(certChain) + assertErrorEqual(expectedErr, err, t) + + certChain = []*x509.Certificate{timestamp_leaf, timestamp_intermediate, timestamp_root, timestamp_root} + expectedErr = "intermediate certificate with subject \"CN=DigiCert Trusted Root G4,OU=www.digicert.com,O=DigiCert Inc,C=US\" is self-signed. Certificate chain must not contain self-signed intermediate certificate" + err = ValidateTimestampingCertChain(certChain) + assertErrorEqual(expectedErr, err, t) +} + +var ekuNonCriticalTimeLeafPem = "-----BEGIN CERTIFICATE-----\n" + + "MIIC5TCCAc2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1JbnRl\n" + + "cm1lZGlhdGUyMCAXDTIyMDYzMDE5MjAwNFoYDzMwMjExMDMxMTkyMDA0WjAbMRkw\n" + + "FwYDVQQDDBBUaW1lU3RhbXBpbmdMZWFmMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n" + + "MIIBCgKCAQEAyx2ispY5C5sQCiLAuCUTp4wv+fpgHwzE4an8eqi+Jrm0tEabTdzP\n" + + "IdZFRYPZbgRx+D9DKeN76f+rt51G9gOX77fYWyIXgnVL4UAYNlQj58hqZ0IO22vT\n" + + "nIFiDbJoSPuamQaLZNuluiirUwJv1uqSQiEnWHC4LhKwNOo4UHH5S3XkkYRpdFBF\n" + + "Tm4uOTaQJA9dfCh+0wbe7ZlEjDiuk1GTSQu69EPIl4IK7aEWqdvk2z1Pg4YkgJZX\n" + + "mWzkECNayUiBeHj7lL5ZnyZeki2l77WzXe/j5dgQ9E2+63hfBew+O/XeS/Tm/TyQ\n" + + "0P8bQre6vbn9820Cpyg82fd1+5bwYedwVwIDAQABozUwMzAOBgNVHQ8BAf8EBAMC\n" + + "B4AwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDANBgkqhkiG9w0B\n" + + "AQsFAAOCAQEAB9Z80K17p4J3VCqVcKyhgkzzYPoKiBWFThVwxS2+TKY0x4zezSAT\n" + + "69Nmf7NkVH4XyvCEUfgdWYst4t41rH3b5MTMOc5/nPeMccDWT0eZRivodF5hFWZd\n" + + "2QSFiMHmfUhnglY0ocLbfKeI/QoSGiPyBWO0SK6qOszRi14lP0TpgvgNDtMY/Jj5\n" + + "AyINT6o0tyYJvYE23/7ysT3U6pq50M4vOZiSuRys83As/qvlDIDKe8OVlDt6xRvr\n" + + "fqdMFWSk6Iay2OCfYcjUbTutMzSI7dvhDivn5FKnNA6M7QD1lqb7V9fymgrQTsth\n" + + "We9tUxypXgMjYN74QEHYxEAIfNOTeBppWw==\n" + + "-----END CERTIFICATE-----" +var ekuNonCriticalTimeLeafCert = parseCertificateFromString(ekuNonCriticalTimeLeafPem) + +func TestTimestampLeafWithNonCriticalEKU(t *testing.T) { + expectedErr := "timestamp signing certificate with subject \"CN=TimeStampingLeaf\" must have extended key usage extension marked as critical" + err := validateTimestampingLeafCertificate(ekuNonCriticalTimeLeafCert) + assertErrorEqual(expectedErr, err, t) +} + +var ekuWrongValuesTimeLeafPem = "-----BEGIN CERTIFICATE-----\n" + + "MIIC6jCCAdKgAwIBAgIJAJOlT2AUbsZiMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTcyM1oYDzIxMjIwNjAxMDMxNzIzWjAQMQ4w\n" + + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOZe\n" + + "9zjKWNlFD/HGrkaAI9mh9Fw1gF8S2tphQD/aPd9IS4HJJEQRkKz5oeHj2g1Y6TEk\n" + + "plODrKlnoLe+ZFNFFD4xMVV55aQSJDTljCLPwIZt2VewlaAhIImYihOJvJFST1zW\n" + + "K2NW4eLxt0awbE/YzL6beH4A6UsrcXcnN0KKiu6YD1/d5TezJoTQBMo6fboltuce\n" + + "P/+RMxyqpvip7nyFF3Yrmhumb7DKJrmSfSjdziI5QoUqzqVgqJ8pXMRb3ZOKb499\n" + + "d9RRxGkox93iOdSSlaP3FEl8VK9KqnD+MNhjVZbeYTfjm9UVdp91VLP1E/yfMXz+\n" + + "fZhYkublK6v3GWSEcb0CAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgeAMDEGA1UdJQQq\n" + + "MCgGCCsGAQUFBwMIBggrBgEFBQcDAQYIKwYBBQUHAwQGCCsGAQUFBwMIMA0GCSqG\n" + + "SIb3DQEBCwUAA4IBAQCaQZ+ws93F1azT6SKBYvBRBCj07+2DtNI83Q53GxrVy2vU\n" + + "rP1ULX7beY87amy6kQcqnQ0QSaoLK+CDL88pPxR2PBzCauz70rMRY8O/KrrLcfwd\n" + + "D5HM9DcbneqXQyfh0ZQpt0wK5wux0MFh2sAEv76jgYBMHq2zc+19skAW/oBtTUty\n" + + "i/IdOVeO589KXwJzEJmKiswN9zKo9KGgAlKS05zohjv40AOCAs+8Q2lOJjRMq4Ji\n" + + "z21qor5e/5+NnGY+2p4A7PbN+QnDdRC3y16dESRN50o5x6CwUWQO74+uRjrAWYCm\n" + + "f/Y7qdOf5zZbY21n8KnLcFOsKhwv4t40Y/LQqN/L\n" + + "-----END CERTIFICATE-----" +var ekuWrongValuesTimeLeaf = parseCertificateFromString(ekuWrongValuesTimeLeafPem) + +func TestFailEkuWrongValuesTimeLeaf(t *testing.T) { + err := validateTimestampingLeafCertificate(ekuWrongValuesTimeLeaf) + assertErrorEqual("timestamp signing certificate with subject \"CN=Hello\" must have and only have Timestamping as extended key usage", err, t) +} + +var ekuMissingTimestampingLeafPem = "-----BEGIN CERTIFICATE-----\n" + + "MIICzDCCAbSgAwIBAgIJAJtYOfTu82KRMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTMxM1oYDzIxMjIwNjAxMDMxMzEzWjAQMQ4w\n" + + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALQN\n" + + "GJKHE6cdcmrHkxXOTawWgYEF1X42IOK7gAXFg+KBPHPw4npDjUclLX0sY3XjBuhT\n" + + "wI5DRATSNTV2ba3+DpFuH3D+Hbfjil91AG8XzormUPOOCbZqJxSKYAIZfPQGdUvV\n" + + "UBulnbDsije00HoNZ03IvdjxbB/9y6a3qQEvIUaEjaZBH3s/YYQIiEmKu6eDpj3R\n" + + "PnUcrP5b7jBMA/Vb8joLM0InzqGPRLPFAPf5womAjxZSsrgyVeA1xSm+6KtXMmaA\n" + + "IKYwNVAOnhfqgUk0tlaRyXXji2T1M9w9l5XUA1iNOMcjTUTfFa5KW7c0TLTcK6vW\n" + + "Eq1BEXUEw7HP7DQUjycCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM\n" + + "MAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4IBAQCSr6A/YAMd6lisgipR0UCA\n" + + "4Ye/1kl0jglT7stLTfftSeXgCKXYlwus9VSpZBtg+RvJkihlLNT6vtsiTMfJUBBc\n" + + "jALLKYUQuCw9sReAbfvecIfc2bUve6X8isLWDVnxlC1udx2WG3lIfW2Sgs/dYeZW\n" + + "yqLTagK5GLlDfg9gBpHLmQYOmshhI85ObOioUAiWTW+S6mx4Bphgl7dlcUabJxEJ\n" + + "MpJJiGPkUUUCuYkp31E7S4JRbSXSkaHefZxB5fvhlbnACeqnOtMG/IKaTjCUemkK\n" + + "ZRmJ0Al1PTWs+Dn8zLzexP/LkmQZU/FUMxeat/dAnc2blDbVnAsvcvnutXGHoZH5\n" + + "-----END CERTIFICATE-----" +var ekuMissingTimestampingLeaf = parseCertificateFromString(ekuMissingTimestampingLeafPem) + +func TestFailEkuMissingTimestampingLeaf(t *testing.T) { + err := validateTimestampingLeafCertificate(ekuMissingTimestampingLeaf) + assertErrorEqual("timestamp signing certificate with subject \"CN=Hello\" must have and only have Timestamping as extended key usage", err, t) +} + +func TestTimestampingFailNoBasicConstraintsCa(t *testing.T) { + err := validateTimestampingCACertificate(noBasicConstraintsCa, 3) + assertErrorEqual("certificate with subject \"CN=Hello\": ca field in basic constraints must be present, critical, and set to true", err, t) +} + +func TestTimestampingFailKuMissingCa(t *testing.T) { + err := validateTimestampingCACertificate(kuMissingCa, 3) + assertErrorEqual("certificate with subject \"CN=Hello\": key usage extension must be present", err, t) +} + +func TestTimestampingFailInvalidPathLenCa(t *testing.T) { + err := validateTimestampingCACertificate(rootCert, 3) + assertErrorEqual("certificate with subject \"CN=Root\": expected path length of 3 but certificate has path length 2 instead", err, t) +} + +func TestTimestampingFailKuNotCertSignCa(t *testing.T) { + err := validateTimestampingCACertificate(kuNotCertSignCa, 3) + assertErrorEqual("certificate with subject \"CN=Hello\": key usage must have the bit positions for key cert sign set", err, t) +} + +func TestTimestampingFailWrongExtendedKeyUsage(t *testing.T) { + err := validateTimestampingLeafCertificate(validNoOptionsLeaf) + assertErrorEqual("timestamp signing certificate with subject \"CN=Hello\" must have and only have Timestamping as extended key usage", err, t) +} + +func TestValidateTimestampingLeafCertificate(t *testing.T) { + err := validateTimestampingLeafCertificate(caTrueLeaf) + assertErrorEqual("certificate with subject \"CN=Hello\": if the basic constraints extension is present, the ca field must be set to false", err, t) + + err = validateTimestampingLeafCertificate(kuNoDigitalSignatureLeaf) + assertErrorEqual("the certificate with subject \"CN=Hello\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", err, t) + + cert := &x509.Certificate{ + Subject: pkix.Name{CommonName: "Test CN"}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + err = validateTimestampingLeafCertificate(cert) + assertErrorEqual("certificate with subject \"CN=Test CN\": key usage extension must be present", err, t) +} + +func TestEkuToString(t *testing.T) { + if ekuToString(x509.ExtKeyUsageAny) != "Any" { + t.Fatalf("expected Any") + } + if ekuToString(x509.ExtKeyUsageClientAuth) != "ClientAuth" { + t.Fatalf("expected ClientAuth") + } + if ekuToString(x509.ExtKeyUsageEmailProtection) != "EmailProtection" { + t.Fatalf("expected EmailProtection") + } + if ekuToString(x509.ExtKeyUsageCodeSigning) != "CodeSigning" { + t.Fatalf("expected CodeSigning") + } + if ekuToString(x509.ExtKeyUsageIPSECUser) != "7" { + t.Fatalf("expected 7") + } +}