@@ -34,18 +34,35 @@ import (
3434
3535 "github.com/smallstep/pkcs7"
3636 expirationcache "k8s.io/client-go/tools/cache"
37+ "k8s.io/klog/v2"
3738)
3839
3940const (
4041 // attestedDocumentTimeFormat is the timestamp format used by the Azure IMDS attested document.
4142 attestedDocumentTimeFormat = "01/02/06 15:04:05 -0700"
4243
44+ // attestedDocumentMaxAge bounds how old an IMDS attested document may be,
45+ // independent of the service-provided expiration. This narrows the replay
46+ // window when the nonce is derived from deterministic request content.
47+ attestedDocumentMaxAge = 5 * time .Minute
48+
49+ // attestedDocumentMaxClockSkew allows a modest amount of node/controller
50+ // clock skew on all verifier-vs-local-time checks before rejecting the document.
51+ attestedDocumentMaxClockSkew = 2 * time .Minute
52+
4353 // attestedDocumentNonceLength is the length of the hex-encoded SHA256 prefix
4454 // used as the Azure IMDS attested document nonce. IMDS enforces a 32-character
4555 // maximum for the nonce parameter; 32 hex chars is 128 bits of entropy, well
4656 // above the cryptographic nonce floor.
4757 attestedDocumentNonceLength = 32
4858
59+ // azureMetadataDNSName and azureMetadataSubdomainSuffix restrict the PKCS7
60+ // signer certificate to AzureCloud (public) metadata endpoints.
61+ // TODO: When sovereign cloud environments are supported, add their domains:
62+ // - metadata.azure.us (AzureUSGovernment)
63+ // - metadata.azure.cn (AzureChinaCloud)
64+ // - metadata.microsoftazure.de (AzureGermanCloud)
65+ // https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service#signature-validation-guidance
4966 azureMetadataDNSName = "metadata.azure.com"
5067 azureMetadataSubdomainSuffix = ".metadata.azure.com"
5168
@@ -132,8 +149,9 @@ type attestedTimeStamp struct {
132149}
133150
134151// verifyAttestedDocument verifies a PKCS7 attested document using the system
135- // root certificate pool, first trying the embedded PKCS7 chain and only
136- // falling back to Microsoft PKI when intermediates are missing.
152+ // root certificate pool, validating cheap signed-content checks before chain
153+ // building and consulting Microsoft PKI only if the embedded PKCS7 chain does
154+ // not already provide the required issuer certificate.
137155func verifyAttestedDocument (signature string , body []byte ) (* attestedData , error ) {
138156 rootCertPool , err := systemCertPool ()
139157 if err != nil {
@@ -143,10 +161,14 @@ func verifyAttestedDocument(signature string, body []byte) (*attestedData, error
143161 return verifyAttestedDocumentWithRootAndFetcher (signature , body , rootCertPool , intermediateCertPoolForSigner )
144162}
145163
164+ // intermediateCertPoolForSigner returns intermediates for the signer's issuer
165+ // using the package-level TTL caches, fetching from Microsoft PKI on miss.
166+ func intermediateCertPoolForSigner (signer * x509.Certificate ) (* x509.CertPool , error ) {
167+ return intermediateCertPoolWithCaches (signer , fetchIntermediateCerts , intermediateCertPositiveCache , intermediateCertNegativeCache )
168+ }
169+
146170// verifyAttestedDocumentWithRootAndFetcher verifies a PKCS7 attested document
147- // using the supplied root pool and intermediate fetcher. It prefers the
148- // embedded PKCS7 certificates and only consults the fetcher when the embedded
149- // chain does not already verify.
171+ // using the supplied root pool and intermediate fetcher.
150172func verifyAttestedDocumentWithRootAndFetcher (signature string , body []byte , rootCertPool * x509.CertPool , fetchIntermediates func (* x509.Certificate ) (* x509.CertPool , error )) (* attestedData , error ) {
151173 if rootCertPool == nil {
152174 return nil , fmt .Errorf ("root certificate pool is required" )
@@ -160,41 +182,36 @@ func verifyAttestedDocumentWithRootAndFetcher(signature string, body []byte, roo
160182 return nil , err
161183 }
162184
163- embeddedOnlyIntermediates := x509 .NewCertPool ()
164- if err := verifySignerCertChain (signer , p7 .Certificates , rootCertPool , embeddedOnlyIntermediates ); err != nil {
165- // Signer SAN was validated above; only now is it safe to hit the network.
166- intermediateCerts , fetchErr := fetchIntermediates (signer )
167- if fetchErr != nil {
168- return nil , fmt .Errorf ("verifying PKCS7 certificate chain: embedded=%w; fetch=%w" , err , fetchErr )
169- }
170- if err := verifySignerCertChain (signer , p7 .Certificates , rootCertPool , intermediateCerts ); err != nil {
171- return nil , fmt .Errorf ("verifying PKCS7 certificate chain: %w" , err )
172- }
185+ // The PKCS7 signature is already integrity-checked above, so it is safe to
186+ // parse the signed content now for rejection-only checks like nonce and time
187+ // bounds before paying for chain building or network fetches.
188+ data , err := parseAndValidateAttestedDocumentContent (p7 .Content , body )
189+ if err != nil {
190+ return nil , err
173191 }
174192
175- return parseAndValidateAttestedDocumentContent (p7 .Content , body )
176- }
177-
178- // verifyAttestedDocumentWithCertPools verifies a base64-encoded PKCS7 attested
179- // document using caller-supplied root and intermediate certificate pools.
180- func verifyAttestedDocumentWithCertPools (signature string , body []byte , rootCertPool * x509.CertPool , intermediateCerts * x509.CertPool ) (* attestedData , error ) {
181- if rootCertPool == nil {
182- return nil , fmt .Errorf ("root certificate pool is required" )
183- }
184- if intermediateCerts == nil {
185- return nil , fmt .Errorf ("intermediate certificate pool is required" )
193+ if err := verifySignerCertChain (signer , p7 .Certificates , rootCertPool , x509 .NewCertPool ()); err == nil {
194+ klog .V (2 ).Infof ("PKCS7 certificate chain verified with embedded certificates for signer issuer %q" , signer .Issuer )
195+ return data , nil
196+ } else {
197+ for _ , cert := range p7 .Certificates {
198+ if validateFetchedIntermediateForSigner (signer , cert ) == nil {
199+ return nil , fmt .Errorf ("verifying PKCS7 certificate chain: %w" , err )
200+ }
201+ }
186202 }
187203
188- p7 , signer , err := parseAndValidatePKCS7Signer (signature )
204+ klog .V (2 ).Infof ("Resolving intermediate certificates for signer issuer %q" , signer .Issuer )
205+ intermediateCerts , err := fetchIntermediates (signer )
189206 if err != nil {
190- return nil , err
207+ return nil , fmt . Errorf ( "fetching intermediate certificates: %w" , err )
191208 }
192-
193209 if err := verifySignerCertChain (signer , p7 .Certificates , rootCertPool , intermediateCerts ); err != nil {
194210 return nil , fmt .Errorf ("verifying PKCS7 certificate chain: %w" , err )
195211 }
212+ klog .V (2 ).Infof ("PKCS7 certificate chain verified after resolving intermediate certificates for signer issuer %q" , signer .Issuer )
196213
197- return parseAndValidateAttestedDocumentContent ( p7 . Content , body )
214+ return data , nil
198215}
199216
200217// parseAndValidatePKCS7Signer decodes and parses a base64-encoded PKCS7
@@ -211,24 +228,29 @@ func parseAndValidatePKCS7Signer(signature string) (*pkcs7.PKCS7, *x509.Certific
211228 if err != nil {
212229 return nil , nil , fmt .Errorf ("decoding PKCS7 signature: %w" , err )
213230 }
231+ klog .V (4 ).Infof ("Decoded PKCS7 signature (%d bytes)" , len (sigBytes ))
214232
215233 p7 , err := pkcs7 .Parse (sigBytes )
216234 if err != nil {
217235 return nil , nil , fmt .Errorf ("parsing PKCS7 signature: %w" , err )
218236 }
237+ klog .V (8 ).Infof ("Parsed PKCS7 structure with %d embedded certificate(s)" , len (p7 .Certificates ))
219238
220239 // Verify the PKCS7 signature against the embedded leaf certificate.
221240 if err := p7 .Verify (); err != nil {
222241 return nil , nil , fmt .Errorf ("verifying PKCS7 signature: %w" , err )
223242 }
243+ klog .V (4 ).Infof ("PKCS7 self-signature verified" )
224244
225245 signer := p7 .GetOnlySigner ()
226246 if signer == nil {
227247 return nil , nil , fmt .Errorf ("getting PKCS7 signer certificate" )
228248 }
249+ klog .V (8 ).Infof ("PKCS7 signer certificate: subject=%q issuer=%q SANs=%v" , signer .Subject , signer .Issuer , signer .DNSNames )
229250 if err := validateAzureMetadataSignerSAN (signer ); err != nil {
230251 return nil , nil , fmt .Errorf ("validating PKCS7 signer SAN: %w" , err )
231252 }
253+ klog .V (4 ).Infof ("PKCS7 signer SAN validated as Azure metadata endpoint" )
232254
233255 return p7 , signer , nil
234256}
@@ -241,28 +263,58 @@ func nonceForBody(body []byte) string {
241263}
242264
243265// parseAndValidateAttestedDocumentContent unmarshals the signed attestation
244- // payload and validates its nonce and expiration .
266+ // payload and validates its nonce and freshness timestamps .
245267func parseAndValidateAttestedDocumentContent (content []byte , body []byte ) (* attestedData , error ) {
246- // Extract and validate the signed attested data.
268+ // Parse the signed attested data.
247269 var data attestedData
248270 if err := json .Unmarshal (content , & data ); err != nil {
249271 return nil , fmt .Errorf ("unmarshalling attested data: %w" , err )
250272 }
273+ klog .V (4 ).Infof ("Attested document content: vmId=%q subscriptionId=%q createdOn=%q expiresOn=%q" , data .VMId , data .SubscriptionId , data .TimeStamp .CreatedOn , data .TimeStamp .ExpiresOn )
274+
275+ if data .VMId == "" {
276+ return nil , fmt .Errorf ("attested document vmId is required" )
277+ }
278+ if data .SubscriptionId == "" {
279+ return nil , fmt .Errorf ("attested document subscriptionId is required" )
280+ }
251281
252282 // Verify the nonce matches the request body hash (replay protection).
253- if data .Nonce != nonceForBody (body ) {
254- return nil , fmt .Errorf ("attested document nonce mismatch" )
283+ expectedNonce := nonceForBody (body )
284+ if data .Nonce != expectedNonce {
285+ return nil , fmt .Errorf ("attested document nonce mismatch: got=%q expected=%q" , data .Nonce , expectedNonce )
255286 }
256287
257- // Verify the attested document has not expired.
288+ now := time .Now ().UTC ()
289+ if data .TimeStamp .CreatedOn == "" {
290+ return nil , fmt .Errorf ("attested document createdOn is required" )
291+ }
292+ createdOn , err := time .Parse (attestedDocumentTimeFormat , data .TimeStamp .CreatedOn )
293+ if err != nil {
294+ return nil , fmt .Errorf ("parsing attested document creation: %w" , err )
295+ }
296+ if createdOn .After (now .Add (attestedDocumentMaxClockSkew )) {
297+ return nil , fmt .Errorf ("attested document createdOn %s is too far in the future" , data .TimeStamp .CreatedOn )
298+ }
299+ oldestAllowedCreatedOn := now .Add (- (attestedDocumentMaxAge + attestedDocumentMaxClockSkew ))
300+ if createdOn .Before (oldestAllowedCreatedOn ) {
301+ return nil , fmt .Errorf ("attested document createdOn %s is older than allowed freshness window of %s plus %s clock skew" , data .TimeStamp .CreatedOn , attestedDocumentMaxAge , attestedDocumentMaxClockSkew )
302+ }
303+ klog .V (4 ).Infof ("Attested document createdOn is fresh (createdOn=%s now=%s)" , createdOn .Format (time .RFC3339 ), now .Format (time .RFC3339 ))
304+
305+ // Verify the attested document has not expired and has a coherent lifetime.
258306 if data .TimeStamp .ExpiresOn != "" {
259307 expiresOn , err := time .Parse (attestedDocumentTimeFormat , data .TimeStamp .ExpiresOn )
260308 if err != nil {
261309 return nil , fmt .Errorf ("parsing attested document expiration: %w" , err )
262310 }
263- if time .Now ().After (expiresOn ) {
311+ if expiresOn .Before (createdOn ) {
312+ return nil , fmt .Errorf ("attested document expiresOn %s is before createdOn %s" , data .TimeStamp .ExpiresOn , data .TimeStamp .CreatedOn )
313+ }
314+ if expiresOn .Before (now .Add (- attestedDocumentMaxClockSkew )) {
264315 return nil , fmt .Errorf ("attested document expired at %s" , data .TimeStamp .ExpiresOn )
265316 }
317+ klog .V (4 ).Infof ("Attested document not expired (expiresOn=%s)" , expiresOn .Format (time .RFC3339 ))
266318 }
267319
268320 return & data , nil
@@ -287,12 +339,6 @@ func systemCertPool() (*x509.CertPool, error) {
287339 return pool , nil
288340}
289341
290- // intermediateCertPoolForSigner returns intermediates for the signer's issuer
291- // using the package-level TTL caches, fetching from Microsoft PKI on miss.
292- func intermediateCertPoolForSigner (signer * x509.Certificate ) (* x509.CertPool , error ) {
293- return intermediateCertPoolWithCaches (signer , fetchIntermediateCerts , intermediateCertPositiveCache , intermediateCertNegativeCache )
294- }
295-
296342// intermediateCertPoolWithCaches performs a cached lookup against the supplied
297343// positive and negative TTL caches, invoking fetch on a miss. Tests inject
298344// their own stores and fetchers.
@@ -319,12 +365,14 @@ func intermediateCertPoolWithCaches(signer *x509.Certificate, fetch func(*x509.C
319365 // Positive cache wins over negative: a successful later fetch overwrites
320366 // any stale negative entry, which expires on its own shorter TTL.
321367 if obj , ok , _ := positive .GetByKey (keyStr ); ok {
368+ klog .V (4 ).Infof ("Intermediate certificate cache hit (positive) for signer issuer %q" , signer .Issuer )
322369 return obj .(* intermediateCertCacheEntry ).pool , nil
323370 }
324371 if _ , ok , _ := negative .GetByKey (keyStr ); ok {
325- return nil , fmt .Errorf ("intermediate certificate fetch recently failed for signer issuer (cached)" )
372+ return nil , fmt .Errorf ("intermediate certificate fetch recently failed for signer issuer %q (cached)" , signer . Issuer )
326373 }
327374
375+ klog .V (2 ).Infof ("Intermediate certificate cache miss for signer issuer %q; fetching from Microsoft PKI" , signer .Issuer )
328376 pool , fetchErr := fetch (signer )
329377 entry .pool = pool
330378 if fetchErr != nil {
@@ -373,19 +421,23 @@ func fetchIntermediateCertsFromBaseURL(client *http.Client, baseURL string, sign
373421 pool := x509 .NewCertPool ()
374422 matched := 0
375423 for _ , url := range urls {
424+ klog .V (2 ).Infof ("Fetching intermediate certificate from %s" , url )
376425 cert , err := fetchCertificate (client , url )
377426 if err != nil {
378427 return nil , err
379428 }
380429 if err := validateFetchedIntermediateForSigner (signer , cert ); err != nil {
430+ klog .V (2 ).Infof ("Fetched intermediate certificate from %s did not match signer issuer: %v" , url , err )
381431 continue
382432 }
433+ klog .V (2 ).Infof ("Fetched intermediate certificate from %s matched signer issuer" , url )
383434 pool .AddCert (cert )
384435 matched ++
385436 }
386437 if matched == 0 {
387- return nil , fmt .Errorf ("no fetched intermediate certificates matched signer issuer" )
438+ return nil , fmt .Errorf ("no fetched intermediate certificates matched signer issuer %q" , signer . Issuer )
388439 }
440+ klog .V (2 ).Infof ("Fetched %d intermediate certificate(s) matching signer issuer %q from %d candidate URL(s)" , matched , signer .Issuer , len (urls ))
389441
390442 return pool , nil
391443}
0 commit comments