Skip to content

Commit ea903e7

Browse files
committed
TMP
Signed-off-by: Ciprian Hacman <ciprian@hakman.dev>
1 parent 7ad7d32 commit ea903e7

6 files changed

Lines changed: 455 additions & 107 deletions

File tree

upup/pkg/fi/cloudup/azure/attest.go

Lines changed: 96 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -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

3940
const (
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.
137155
func 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.
150172
func 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.
245267
func 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

Comments
 (0)