Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create the Python Track 2 SDK for the Microsoft Azure Attestation Service. #18023

Merged
merged 69 commits into from
May 7, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
5c676c3
Added local autorest generation file for python
LarryOsterman Apr 9, 2021
1d57996
Extremely bare bones implementation of the start of a python SDK for …
LarryOsterman Apr 9, 2021
21a1069
Added local autorest generation file for python
LarryOsterman Apr 9, 2021
29b1b2d
Extremely bare bones implementation of the start of a python SDK for …
LarryOsterman Apr 9, 2021
deb3fbd
Merge branch 'LarryO-CreatePythonSDK' of https://github.com/LarryOste…
LarryOsterman Apr 12, 2021
38739ee
Added get_policy API and some more infrastructure
LarryOsterman Apr 12, 2021
7bdbb8d
Python starts to limp - get_policy and set_policy implemented
LarryOsterman Apr 13, 2021
cbd6717
Merge branch 'master' into LarryO-CreatePythonSDK
LarryOsterman Apr 13, 2021
6ba4a1e
Fixed typo
LarryOsterman Apr 13, 2021
bcf96d5
CI Test pass fixes
LarryOsterman Apr 13, 2021
1b50afa
Test pass setup
LarryOsterman Apr 13, 2021
3487871
Removed duplicate attestation_location_short_name
LarryOsterman Apr 13, 2021
5e1a5d3
More CI pipeline fixes
LarryOsterman Apr 13, 2021
5409a03
Converted async test to use TestPreparer; re-ran recorded tests
LarryOsterman Apr 13, 2021
d517396
Only check issuer on live tests
LarryOsterman Apr 13, 2021
98fe4fd
Updates to unit tests, recorded tests now all pass
LarryOsterman Apr 14, 2021
4df18a1
Removed Python 3 specific constructs
LarryOsterman Apr 14, 2021
a63cbb4
A couple more Py3 constructs
LarryOsterman Apr 14, 2021
eb68997
More Py3 constructs
LarryOsterman Apr 14, 2021
af65901
Hopefully final Py3 constructs
LarryOsterman Apr 14, 2021
33635d4
Try passing bytes into base64 decoder, not str
LarryOsterman Apr 14, 2021
a6b3791
Another Py3/Py27 fix
LarryOsterman Apr 14, 2021
08362e1
Try to remove envvars from tests.yml
LarryOsterman Apr 14, 2021
7f6a4b5
Try to remove envvars from tests.yml
LarryOsterman Apr 14, 2021
3da92db
Validate attestation tokens
LarryOsterman Apr 15, 2021
22f6ed8
Convert bytes to str to make json encoder happy
LarryOsterman Apr 15, 2021
0b91fbb
Changed how we convert base64 to string
LarryOsterman Apr 15, 2021
110c66d
Added test for secured and unsecured policy set
LarryOsterman Apr 15, 2021
4bfbe1d
Added attestation team to codeowners
LarryOsterman Apr 15, 2021
daa6aa7
Pass in type of AttestationToken into constructor, making it dramatic…
LarryOsterman Apr 16, 2021
26f4cd6
Start converting attributes in AttestationToken to properties
LarryOsterman Apr 16, 2021
2ac1c55
Added support for sgx and openenclave, and tests for both
LarryOsterman Apr 17, 2021
2dcb2bc
Added kwargs to all the calls into generated code
LarryOsterman Apr 19, 2021
e7ed6ad
Updated recordings
LarryOsterman Apr 19, 2021
a059294
Merge branch 'master' into LarryO-CreatePythonSDK
LarryOsterman Apr 19, 2021
521c1d1
Update sdk/attestation/azure-security-attestation/SWAGGER.md
LarryOsterman Apr 19, 2021
27492ca
Update sdk/attestation/azure-security-attestation/azure/security/atte…
LarryOsterman Apr 19, 2021
9fd1db6
Update sdk/attestation/azure-security-attestation/azure/security/atte…
LarryOsterman Apr 19, 2021
38ab274
Update sdk/attestation/azure-security-attestation/azure/security/atte…
LarryOsterman Apr 19, 2021
8e52d67
Update sdk/attestation/azure-security-attestation/tests/test_attestat…
LarryOsterman Apr 19, 2021
e399c3e
Next round of pull request feedback
LarryOsterman Apr 19, 2021
ad5a401
Integrated first round of API changes from .Net API review; Removed C…
LarryOsterman May 4, 2021
1cff859
Client documentation updates
LarryOsterman May 4, 2021
b91bb79
Ported readme.md from .Net to Python - snippets are still .Net unfort…
LarryOsterman May 4, 2021
e8efb6b
Sphinx updates
LarryOsterman May 4, 2021
9d48ebf
Updates to readme.md to include python snippets
LarryOsterman May 5, 2021
92a4a54
More readme CI changes
LarryOsterman May 5, 2021
c94d7ff
Removed en-us from link
LarryOsterman May 5, 2021
07aae3c
Finished attestation callback test; re-generated swagger; moved swagg…
LarryOsterman May 5, 2021
1836e36
Working and tests for add policy certificate
LarryOsterman May 6, 2021
3ccee2b
CI Fixes; cleaned up more configuration options
LarryOsterman May 6, 2021
1ef5652
Added attestation swagger to docsettings
LarryOsterman May 6, 2021
545bbda
Updated recordings; added TPM attestation test; added remove policy m…
LarryOsterman May 6, 2021
2c6d43e
Added recordings for TPM attestation
LarryOsterman May 6, 2021
fc4c333
Copyright note updates
LarryOsterman May 6, 2021
df6c230
Fixed typos in CODEOWNERS file
LarryOsterman May 6, 2021
cc55553
Update sdk/attestation/azure-security-attestation/azure/security/atte…
LarryOsterman May 6, 2021
8f469a2
Update sdk/attestation/azure-security-attestation/azure/security/atte…
LarryOsterman May 6, 2021
724aa34
Update sdk/attestation/azure-security-attestation/azure/security/atte…
LarryOsterman May 6, 2021
a60e1e8
Update sdk/attestation/azure-security-attestation/azure/security/atte…
LarryOsterman May 7, 2021
516ff1f
Removed a couple of ._generated exported types; cleaned up token vali…
LarryOsterman May 7, 2021
df8c4c1
Removed generated types from __init__.py
LarryOsterman May 7, 2021
25a6542
More pull request feedback
LarryOsterman May 7, 2021
d71ea80
Tweaks to client API - cleaned up some documentation; use list to fil…
LarryOsterman May 7, 2021
2f6e9aa
Merge branch 'master' into LarryO-CreatePythonSDK
LarryOsterman May 7, 2021
3acce02
More documentation cleanup; minor changes to async test
LarryOsterman May 7, 2021
6a5cbd0
lmanzuel first round of pull request feedback
LarryOsterman May 7, 2021
cadb558
Removed base_url attrbute/property
LarryOsterman May 7, 2021
c0e4d88
Attestation requires cryptography
LarryOsterman May 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Added support for sgx and openenclave, and tests for both
  • Loading branch information
LarryOsterman committed Apr 17, 2021
commit 2ac1c55f7546acb62aecf5d6cf4dd9a1b0bdc109
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ def set_policy(self, attestation_type, attestation_policy, signing_key=None):
return AttestationResponse[PolicyResult](token, token.get_body())


@distributed_trace
def _get_signers(self):
#type() -> List[AttestationSigner]
""" Returns the set of signing certificates used to sign attestation tokens.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
from azure.core.pipeline.transport import HttpRequest, HttpResponse

from ._generated import AzureAttestationRestClient
from ._generated.models import AttestationResult, RuntimeData, InitTimeData, DataType, AttestSgxEnclaveRequest, AttestOpenEnclaveRequest
from ._configuration import AttestationClientConfiguration
from ._models import AttestationSigner
from ._models import AttestationSigner, AttestationToken, AttestationResponse
import base64
import cryptography
import cryptography.x509
from typing import List, Any
from azure.core.tracing.decorator import distributed_trace
from threading import Lock


class AttestationClient(object):
Expand All @@ -50,6 +52,8 @@ def __init__(
raise ValueError("Missing credential.")
self._config = AttestationClientConfiguration(credential, instance_url, **kwargs)
self._client = AzureAttestationRestClient(credential, instance_url, **kwargs)
self._statelock = Lock()
self._signing_certificates = None

@distributed_trace
def get_openidmetadata(self):
LarryOsterman marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -76,6 +80,58 @@ def get_signing_certificates(self): # type: () ->List[AttestationSigner]
signers.append(AttestationSigner(certificates, key.kid))
return signers

@distributed_trace
def attest_sgx_enclave(self, quote, init_time_data, init_time_data_is_object, runtime_data, runtime_data_is_object, **kwargs):
# type(bytes, Any, bool, Any, bool) -> AttestationResponse[AttestationResult]
LarryOsterman marked this conversation as resolved.
Show resolved Hide resolved
runtime = RuntimeData(
data=runtime_data,
data_type=DataType.JSON if runtime_data_is_object else DataType.BINARY) if runtime_data is not None else None
inittime = InitTimeData(
data=init_time_data,
data_type=DataType.JSON if init_time_data_is_object else DataType.BINARY) if init_time_data is not None else None
request = AttestSgxEnclaveRequest(quote=quote, init_time_data = inittime, runtime_data = runtime)
result = self._client.attestation.attest_sgx_enclave(request, **kwargs)
token = AttestationToken[AttestationResult](token=result.token,
body_type=AttestationResult)
return AttestationResponse[AttestationResult](token, token.get_body())

@distributed_trace
def attest_open_enclave(self, report, init_time_data, init_time_data_is_object, runtime_data, runtime_data_is_object, **kwargs):
# type(bytes, Any, bool, Any, bool) -> AttestationResponse[AttestationResult]
runtime = RuntimeData(
data=runtime_data,
data_type=DataType.JSON if runtime_data_is_object else DataType.BINARY) if runtime_data is not None else None
LarryOsterman marked this conversation as resolved.
Show resolved Hide resolved
inittime = InitTimeData(
data=init_time_data,
data_type=DataType.JSON if init_time_data_is_object else DataType.BINARY) if init_time_data is not None else None
request = AttestOpenEnclaveRequest(report=report, init_time_data = inittime, runtime_data = runtime)
result = self._client.attestation.attest_open_enclave(request, **kwargs)
token = AttestationToken[AttestationResult](token=result.token,
body_type=AttestationResult)
token.validate_token(self._config.token_validation_options, self._get_signers())
return AttestationResponse[AttestationResult](token, token.get_body())

def _get_signers(self):
#type() -> List[AttestationSigner]
""" Returns the set of signing certificates used to sign attestation tokens.
"""

with self._statelock:
if (self._signing_certificates == None):
seankane-msft marked this conversation as resolved.
Show resolved Hide resolved
signing_certificates = self._client.signing_certificates.get()
self._signing_certificates = []
for key in signing_certificates.keys:
# Convert the returned certificate chain into an array of X.509 Certificates.
certificates = []
for x5c in key.x5_c:
der_cert = base64.b64decode(x5c)
cert = cryptography.x509.load_der_x509_certificate(der_cert)
certificates.append(cert)
self._signing_certificates.append(AttestationSigner(certificates, key.kid))
LarryOsterman marked this conversation as resolved.
Show resolved Hide resolved
signers = self._signing_certificates
return signers


def close(self):
# type: () -> None
self._client.close()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from cryptography.hazmat.primitives.hashes import HashAlgorithm, SHA256
from msrest.exceptions import DeserializationError, SerializationError
from ._common import Base64Url
from ._generated.models import PolicyResult, PolicyCertificatesModificationResult, AttestationResult, StoredAttestationPolicy
from ._generated.models import PolicyResult, PolicyCertificatesModificationResult, AttestationResult, StoredAttestationPolicy, JSONWebKey
from typing import Any, Callable, List, Optional, Type, TypeVar, Generic, Union
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
Expand Down Expand Up @@ -167,60 +167,120 @@ def __init__(self, **kwargs):
self._body = JSONDecoder().decode(self.body_bytes.decode('ascii'))
self._header = JSONDecoder().decode(self.header_bytes.decode('ascii'))

# If the caller didn't specify a body when constructing the class, populate the well known attributes from the token.
if (body is None):
# Populate the standardized fields in the header.
self.content_type = self._header.get('cty')
self.critical = self._header.get('crit') # type: Optional[bool]
self.key_url = self._header.get('jku')
self.type = self._header.get('typ')
self.certificate_thumbprint = self._header.get('x5t')
self.certificate_sha256_thumbprint = self._header.get('x5t#256')
self.x509_url = self._header.get('x5u')

# Populate the standardized fields from the body.
iat = self._body.get('iat')
if (iat is not None):
self.issuance_time = datetime.fromtimestamp(iat)
else:
self.issuance_time = None

nbf = self._body.get('nbf')
if (nbf is not None):
self.not_before_time = datetime.fromtimestamp(nbf)
else:
self.not_before_time = None

self.issuer = self._body.get('iss')

def __str__(self):
return self._token

@property
def algorithm(self):
#type:() -> str
#type:() -> str | None
LarryOsterman marked this conversation as resolved.
Show resolved Hide resolved
""" Json Web Token Header "algorithm". See https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.1 for details.
If the value of Algorithm is "none" it indicates that the token is unsecured.
"""
return self._header.get('alg')

@property
def key_id(self):
#type:() -> str
#type:() -> str | None
""" Json Web Token Header "Key ID". See https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.4 for details.
"""
return self._header.get('kid')

@property
def expiration_time(self):
#type:() -> datetime
#type:() -> datetime | None
""" Expiration time for the token.
"""
exp = self._body.get('exp')
if (exp is not None):
return datetime.fromtimestamp(exp)
return None


@property
def not_before_time(self):
#type:() -> datetime | None
""" Time before which the token is invalid.
"""
nbf = self._body.get('nbf')
if (nbf is not None):
LarryOsterman marked this conversation as resolved.
Show resolved Hide resolved
return datetime.fromtimestamp(nbf)
return None

@property
def issuance_time(self):
#type:() -> datetime | None
""" Time when the token was issued.
"""
iat = self._body.get('iat')
if (iat is not None):
return datetime.fromtimestamp(iat)
return None

@property
def content_type(self):
#type:() -> str | None
""" Json Web Token Header "content type". See https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 for details.
"""
return self._header.get('cty')

@property
def critical(self):
#type() -> # type: Optional[bool]
""" Json Web Token Header "Critical". See https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11 for details."""
return self._header.get('crit')

@property
def key_url(self):
#type:() -> str | None
""" Json Web Token Header "Key URL". See https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.2 for details.
"""
return self._header.get('jku')

@property
def x509_url(self):
#type:() -> str | None
""" Json Web Token Header "X509 URL". See https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.5 for details.
"""
return self._header.get('x5u')

@property
def type(self):
#type:() -> str | None
""" Json Web Token Header "type". See https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.9 for details."""
return self._header.get('typ')

@property
def certificate_thumbprint(self):
#type:() -> str | None
""" The "thumbprint" of the certificate used to sign the request. See https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.7 for details. """
return self._header.get('x5t')

@property
def certificate_sha256_thumbprint(self):
#type:() -> str | None
""" The "thumbprint" of the certificate used to sign the request generated using the SHA256 algorithm. See https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.8 for details."""
return self._header.get('x5t#256')

@property
def issuer(self):
#type:() -> str
""" Json Web Token Body Issuer. See https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.1 for details.
"""
return self._body.get('iss')

@property
def x509_certificate_chain(self):
#type:() -> List[Certificate] | None
""" An array of X.509Certificates which represent a certificate chain used to sign the token. See https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.6 for details."""
x5c = self._header.get('x5c')
if x5c is not None:
return self._get_certificates_from_x5c(x5c)
return None

@property
def json_web_key(self):
#type:() -> JSONWebKey
jwk = self._header.get('jwk')
return JSONWebKey.deserialize(jwk)

def serialize(self):
return self._token
Expand All @@ -230,6 +290,8 @@ def serialize(self):

def validate_token(self, options=None, signing_certificates=None):
# type: (TokenValidationOptions, List[AttestationSigner]) -> bool
""" Validates the attestation token.
"""
if (options is None):
options = TokenValidationOptions(
validate_token=True, validate_signature=True, validate_expiration=True)
Expand All @@ -254,7 +316,8 @@ def validate_token(self, options=None, signing_certificates=None):

def get_body(self):
# type: () -> T

""" Returns the body of the attestation token.
"""
try:
return self._body_type.deserialize(self._body)
except AttributeError:
Expand Down Expand Up @@ -294,11 +357,11 @@ def _get_candidate_signing_certificates(self, signing_certificates):
# If we didn't find a matching key ID in the supplied certificates,
# try the JWS header to see if there might be a corresponding key.
if (len(candidates) == 0):
if self._header['jwk']:
if self._header['jwk']['kid'] == desired_key_id:
if (self._header['jwk']['x5c']):
signers = self._get_signers_from_x5c(
self._header['jwk']['x5c'])
jwk = self.json_web_key
if jwk is not None:
if jwk.kid == desired_key_id:
if (jwk.x5_c):
signers = self._get_certificates_from_x5c(jwk.x5_c)
candidates.append(AttestationSigner(
signers, desired_key_id))
else:
Expand All @@ -309,24 +372,22 @@ def _get_candidate_signing_certificates(self, signing_certificates):
for signer in signing_certificates:
candidates.append(signer)
else:
if self._header.get('jwk'):
if (self._header['jwk'].get('x5c')):
signers = self._get_signers_from_x5c(
self._header['jwk']['x5c'])
candidates.append(AttestationSigner(signers, None))
if self._header.get('x5c'):
signers = self._get_signers_from_x5c(self._header['x5c'])
jwk = self.json_web_key
if jwk.x5_c is not None:
signers = self._get_certificates_from_x5c(
self.json_web_key.x5_c)
candidates.append(AttestationSigner(signers, None))
candidates.append(self.x509_certificate_chain)

return candidates

def _get_signers_from_x5c(self, x5clist):
# type:(List[str]) -> List[AttestationSigner]
signers = list()
for cert in x5clist:
signer = load_der_x509_certificate(base64.b64decode(cert))
signers.append(signer)
return signers
def _get_certificates_from_x5c(self, x5clist):
# type:(List[str]) -> List[Certificate]
certs = list()
for b64cert in x5clist:
cert = load_der_x509_certificate(base64.b64decode(b64cert))
certs.append(cert)
return certs

def _validate_signature(self, candidate_certificates):
# type:(List[AttestationSigner]) -> bool
Expand Down Expand Up @@ -403,10 +464,12 @@ def _create_secured_jwt(body, signer):
"""
header = {
"alg": "RSA256" if isinstance(signer.signing_key, RSAPrivateKey) else "ECDH256",
"x5c": [
base64.b64encode(signer.certificate.public_bytes(
Encoding.DER)).decode('utf-8')
]
"jwk": {
"x5c": [
base64.b64encode(signer.certificate.public_bytes(
Encoding.DER)).decode('utf-8')
]
}
}
json_header = JSONEncoder().encode(header)
return_value = Base64Url.encode(json_header.encode('utf-8'))
Expand Down
Loading