diff --git a/go.mod b/go.mod index ed5b51c7a6..3e4dc0190b 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect github.com/fullstorydev/grpcurl v1.8.6 + github.com/fxamacker/cbor/v2 v2.6.0 // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/golang/mock v1.6.0 github.com/golang/protobuf v1.5.2 diff --git a/go.sum b/go.sum index 1525826f4f..b8c79c535e 100644 --- a/go.sum +++ b/go.sum @@ -253,6 +253,8 @@ github.com/fullstorydev/grpcurl v1.8.1/go.mod h1:3BWhvHZwNO7iLXaQlojdg5NA6SxUDeP github.com/fullstorydev/grpcurl v1.8.2/go.mod h1:YvWNT3xRp2KIRuvCphFodG0fKkMXwaxA9CJgKCcyzUQ= github.com/fullstorydev/grpcurl v1.8.6 h1:WylAwnPauJIofYSHqqMTC1eEfUIzqzevXyogBxnQquo= github.com/fullstorydev/grpcurl v1.8.6/go.mod h1:WhP7fRQdhxz2TkL97u+TCb505sxfH78W1usyoB3tepw= +github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= +github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -761,6 +763,8 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.7 h1:aXiFAgRugfJ27UFDsGJ9DB2FvTC73hlVXFSqq5bo9eU= github.com/urfave/cli v1.22.7/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= diff --git a/x509/x509.go b/x509/x509.go index 0ef7253132..d5f4444de6 100644 --- a/x509/x509.go +++ b/x509/x509.go @@ -39,6 +39,8 @@ // - Support for parsing RSASES-OAEP public keys from certificates // - Ed25519 support: // - Support for parsing and marshaling Ed25519 keys +// - X25519 support: +// - Support for parsing X25519 keys // - General improvements: // - Export and use OID values throughout. // - Export OIDFromNamedCurve(). @@ -321,6 +323,7 @@ const ( ECDSA Ed25519 RSAESOAEP + X25519 ) var publicKeyAlgoName = [...]string{ @@ -329,6 +332,7 @@ var publicKeyAlgoName = [...]string{ ECDSA: "ECDSA", Ed25519: "Ed25519", RSAESOAEP: "RSAESOAEP", + X25519: "X25519", } func (algo PublicKeyAlgorithm) String() string { @@ -584,6 +588,7 @@ var ( OIDPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} OIDPublicKeyRSAObsolete = asn1.ObjectIdentifier{2, 5, 8, 1, 1} OIDPublicKeyEd25519 = oidSignatureEd25519 + OIDPublicKeyX25519 = asn1.ObjectIdentifier{1, 3, 101, 110} ) func getPublicKeyAlgorithmFromOID(oid asn1.ObjectIdentifier) PublicKeyAlgorithm { @@ -598,6 +603,8 @@ func getPublicKeyAlgorithmFromOID(oid asn1.ObjectIdentifier) PublicKeyAlgorithm return RSAESOAEP case oid.Equal(OIDPublicKeyEd25519): return Ed25519 + case oid.Equal(OIDPublicKeyX25519): + return X25519 } return UnknownPublicKeyAlgorithm } @@ -1451,6 +1458,8 @@ func parsePublicKey(algo PublicKeyAlgorithm, keyData *publicKeyInfo, nfe *NonFat return pub, nil case Ed25519: return ed25519.PublicKey(asn1Data), nil + case X25519: + return asn1Data, nil default: return nil, nil } diff --git a/x509util/android.go b/x509util/android.go new file mode 100644 index 0000000000..1e9c2c0518 --- /dev/null +++ b/x509util/android.go @@ -0,0 +1,608 @@ +// Copyright 2021 Google LLC. All Rights Reserved. +// +// 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 x509util + +import ( + "bytes" + "errors" + "fmt" + "time" + "unicode/utf8" + + "github.com/google/certificate-transparency-go/asn1" + "github.com/google/certificate-transparency-go/x509" + "github.com/fxamacker/cbor/v2" +) + +// OIDExtensionAndroidAttestation is the OID value for an X.509 extension that holds +// Android attestation info. +var OIDExtensionAndroidAttestation = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 1, 17} + +// OIDExtensionRkpInfo is the OID value for an X.509 extension that holds Android +// remote key provisioning info. +var OIDExtensionAndroidRkpInfo = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 1, 30} + +// OIDExtensionAndroidVmAttestation is the OID value for an X.509 extension that holds +// Android attestation info for a virtual machine. +var OIDExtensionAndroidVmAttestation = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 1, 29, 1} + +// AndroidAttestationInfo holds attestation information attached to an Android +// hardware-backed key, describing features of the key and the device that issued +// it. See https://developer.android.com/training/articles/security-key-attestation for +// more information. +type AndroidAttestationInfo struct { + AttestationVersion int64 + AttestationSecurityLevel asn1.Enumerated + KeyMintVersion int64 + KeyMintSecurityLevel asn1.Enumerated + AttestationChallenge []byte + UniqueId []byte + SoftwareEnforced AuthorizationList + HardwareEnforced AuthorizationList +} + +// AndroidVmAttestationInfo holds attestation information attached to an Android virtual +// machine. +type AndroidVmAttestationInfo struct { + AttestationChallenge []byte + IsVmSecure bool + VmComponents []AndroidVmComponent +} + +// AndroidVmComponent describes one attested component of an Android VM. +type AndroidVmComponent struct { + Name string + SecurityVersion int64 + CodeHash []byte + AuthorityHash []byte +} + +// RkpProvisioningInfo describes remotely provisioned key information +type RkpProvisioningInfo struct { + CertsSigned30Days int64 `cbor:"1,keyasint"` + Manufacturer *string `cbor:"2,keyasint,omitempty"` +} + +func securityLevelToString(lvl asn1.Enumerated) string { + switch lvl { + case 0: + return "SOFTWARE" + case 1: + return "TRUSTED_ENVIRONMENT" + case 2: + return "STRONGBOX" + } + return fmt.Sprintf("UNKNOWN(%d)", lvl) +} + +// RootOfTrust describes the verified boot state of an Android device at the point +// when a key was created. +type RootOfTrust struct { + VerifiedBootKey []byte + DeviceLocked bool + VerifiedBootState asn1.Enumerated + VerifiedBootHash []byte +} + +func bootStateToString(st asn1.Enumerated) string { + switch st { + case 0: + return "VERIFIED" + case 1: + return "SELF_SIGNED" + case 2: + return "UNVERIFIED" + case 3: + return "FAILED" + } + return fmt.Sprintf("UNKNOWN(%d)", st) +} + +// AuthorizationList holds attributes that describe the restrictions placed on +// an Android hardware-backed key, and which describe the state of the device +// that issued the key. +type AuthorizationList struct { + Purpose []int `asn1:"optional,explicit,tag:1,set"` + Algorithm int `asn1:"optional,explicit,tag:2,default:-1"` + KeySize int `asn1:"optional,explicit,tag:3,default:-1"` + BlockMode []int `asn1:"optional,explicit,tag:4,default:-1"` + Digest []int `asn1:"optional,explicit,tag:5,set"` + Padding []int `asn1:"optional,explicit,tag:6,set"` + CallerNonce asn1.RawValue `asn1:"optional,explicit,tag:7"` + MinMacLength int `asn1:"optional,explicit,tag:8,default:-1"` + EcCurve int `asn1:"optional,explicit,tag:10,default:-1"` + RsaPublicExponent int `asn1:"optional,explicit,tag:200,default:-1"` + MgfDigest []int `asn1:"optional,explicit,tag:203,set"` + RollbackResistance asn1.RawValue `asn1:"optional,explicit,tag:303"` + EarlyBootOnly asn1.RawValue `asn1:"optional,explicit,tag:305"` + ActiveDateTime int64 `asn1:"optional,explicit,tag:400,default:-1"` + OriginationExpireDateTime int64 `asn1:"optional,explicit,tag:401,default:-1"` + UsageExpireDateTime int64 `asn1:"optional,explicit,tag:402,default:-1"` + UsageCountLimit int `asn1:"optional,explicit,tag:405,default:-1"` + UserSecureId int `asn1:"optional,explicit,tag:502,default:-1"` + NoAuthRequired asn1.RawValue `asn1:"optional,explicit,tag:503"` + UserAuthType int `asn1:"optional,explicit,tag:504,default:-1"` + AuthTimeout int `asn1:"optional,explicit,tag:505,default:-1"` + AllowWhileOnBody asn1.RawValue `asn1:"optional,explicit,tag:506"` + TrustedUserPresenceReq asn1.RawValue `asn1:"optional,explicit,tag:507"` + TrustedConfirmationReq asn1.RawValue `asn1:"optional,explicit,tag:508"` + UnlockDeviceReq asn1.RawValue `asn1:"optional,explicit,tag:509"` + CreationDateTime int64 `asn1:"optional,explicit,tag:701,default:-1"` + Origin int `asn1:"optional,explicit,tag:702,default:-1"` + RootOfTrust asn1.RawValue `asn1:"optional,explicit,tag:704"` + OsVersion int `asn1:"optional,explicit,tag:705,default:-1"` + OsPatchlevel int `asn1:"optional,explicit,tag:706,default:-1"` + AttestationApplicationId []byte `asn1:"optional,explicit,tag:709"` + AttestationIdBrand []byte `asn1:"optional,explicit,tag:710"` + AttestationIdDevice []byte `asn1:"optional,explicit,tag:711"` + AttestationIdProduct []byte `asn1:"optional,explicit,tag:712"` + AttestationIdSerial []byte `asn1:"optional,explicit,tag:713"` + AttestationIdImei []byte `asn1:"optional,explicit,tag:714"` + AttestationIdMeid []byte `asn1:"optional,explicit,tag:715"` + AttestationIdManufacturer []byte `asn1:"optional,explicit,tag:716"` + AttestationIdModel []byte `asn1:"optional,explicit,tag:717"` + VendorPatchlevel int `asn1:"optional,explicit,tag:718,default:-1"` + BootPatchlevel int `asn1:"optional,explicit,tag:719,default:-1"` + DeviceUniqueAttestation asn1.RawValue `asn1:"optional,explicit,tag:720"` + IdentityCredentialKey asn1.RawValue `asn1:"optional,explicit,tag:721"` + AttestationIdSecondImei []byte `asn1:"optional,explicit,tag:723"` +} + +// AttestInfoFromCert retrieves and parses an Android attestation information extension +// from a certificate, if present. +func AttestInfoFromCert(cert *x509.Certificate) (*AndroidAttestationInfo, error) { + for _, ext := range cert.Extensions { + if ext.Id.Equal(OIDExtensionAndroidAttestation) { + var attestInfo AndroidAttestationInfo + rest, err := asn1.Unmarshal(ext.Value, &attestInfo) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal attestation info: %v", err) + } else if len(rest) > 0 { + return nil, fmt.Errorf("trailing data (%d bytes) after attestation info", len(rest)) + } + return &attestInfo, nil + } + } + return nil, errors.New("no Android Attestation extension found") +} + +// VmInfoFromCert retrieves and parses an Android VM attestation information extension +// from a certificate, if present. +func VmInfoFromCert(cert *x509.Certificate) (*AndroidVmAttestationInfo, error) { + for _, ext := range cert.Extensions { + if ext.Id.Equal(OIDExtensionAndroidVmAttestation) { + var vmInfo AndroidVmAttestationInfo + rest, err := asn1.Unmarshal(ext.Value, &vmInfo) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal attestation info: %v", err) + } else if len(rest) > 0 { + return nil, fmt.Errorf("trailing data (%d bytes) after attestation info", len(rest)) + } + return &vmInfo, nil + } + } + return nil, errors.New("no Android VM Attestation extension found") +} + +// RkpInfoFromCert retrieves and parses an Android VM attestation information extension +// from a certificate, if present. +func RkpInfoFromCert(cert *x509.Certificate) (*RkpProvisioningInfo, error) { + for _, ext := range cert.Extensions { + if ext.Id.Equal(OIDExtensionAndroidRkpInfo) { + var rkpInfo RkpProvisioningInfo + err := cbor.Unmarshal(ext.Value, &rkpInfo) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal RKP CBOR info: %v", err) + } + return &rkpInfo, nil + } + } + return nil, errors.New("no Android RKP Attestation extension found") +} + +func showAndroidVmAttestation(result *bytes.Buffer, cert *x509.Certificate) { + count, critical := OIDInExtensions(OIDExtensionAndroidVmAttestation, cert.Extensions) + if count == 0 { + return + } + result.WriteString(fmt.Sprintf(" Android VM Information:")) + showCritical(result, critical) + vmInfo, err := VmInfoFromCert(cert) + if err != nil { + result.WriteString(fmt.Sprintf(" Failed to decode VM info: (%s)\n", err)) + return + } + showHex(result, " ", "Attestation Challenge", vmInfo.AttestationChallenge) + result.WriteString(fmt.Sprintf(" Is VM Secure: %t\n", vmInfo.IsVmSecure)) + result.WriteString(fmt.Sprintf(" Components:\n")) + for _, component := range vmInfo.VmComponents { + result.WriteString(fmt.Sprintf(" Component:\n")) + result.WriteString(fmt.Sprintf(" Name: %s\n", component.Name)) + result.WriteString(fmt.Sprintf(" Security Version: %d\n", component.SecurityVersion)) + showHex(result, " ", "Code Hash", component.CodeHash) + showHex(result, " ", "Authority Hash", component.AuthorityHash) + } +} + +func showAndroidRkpInfo(result *bytes.Buffer, cert *x509.Certificate) { + count, critical := OIDInExtensions(OIDExtensionAndroidRkpInfo, cert.Extensions) + if count == 0 { + return + } + result.WriteString(fmt.Sprintf(" Android RKP Information:")) + showCritical(result, critical) + rkpInfo, err := RkpInfoFromCert(cert) + if err != nil { + result.WriteString(fmt.Sprintf(" Failed to CBOR-decode RKP info: (%s)\n", err)) + return + } + result.WriteString(fmt.Sprintf(" Certs Signed Last 30d: %d\n", rkpInfo.CertsSigned30Days)) + if rkpInfo.Manufacturer != nil { + result.WriteString(fmt.Sprintf(" Manufacturer: %s\n", *rkpInfo.Manufacturer)) + } +} + +func showAndroidAttestation(result *bytes.Buffer, cert *x509.Certificate) { + count, critical := OIDInExtensions(OIDExtensionAndroidAttestation, cert.Extensions) + if count == 0 { + return + } + result.WriteString(fmt.Sprintf(" Android Attestation Information:")) + showCritical(result, critical) + attestInfo, err := AttestInfoFromCert(cert) + if err != nil { + result.WriteString(fmt.Sprintf(" Failed to decode attestation info: (%s)\n", err)) + return + } + result.WriteString(fmt.Sprintf(" Attestation Version: %d\n", attestInfo.AttestationVersion)) + result.WriteString(fmt.Sprintf(" Attestation Security Level: %s\n", securityLevelToString(attestInfo.AttestationSecurityLevel))) + result.WriteString(fmt.Sprintf(" KeyMint Version: %d\n", attestInfo.KeyMintVersion)) + result.WriteString(fmt.Sprintf(" KeyMint Security Level: %s\n", securityLevelToString(attestInfo.KeyMintSecurityLevel))) + showHex(result, " ", "Attestation Challenge", attestInfo.AttestationChallenge) + showHex(result, " ", "Unique ID", attestInfo.UniqueId) + result.WriteString(fmt.Sprintf(" Software Enforced:\n")) + showKeyAuthorizations(result, attestInfo.SoftwareEnforced, " ") + result.WriteString(fmt.Sprintf(" Hardware Enforced:\n")) + showKeyAuthorizations(result, attestInfo.HardwareEnforced, " ") + + result.WriteString("\n") +} + +func showKeyAuthorizations(buf *bytes.Buffer, auths AuthorizationList, prefix string) { + if len(auths.Purpose) > 0 { + buf.WriteString(fmt.Sprintf("%sPurpose: %s\n", prefix, enumsToString(auths.Purpose, purposeToString))) + } + if auths.Algorithm != -1 { + buf.WriteString(fmt.Sprintf("%sAlgorithm: %s\n", prefix, algorithmToString(auths.Algorithm))) + } + if auths.KeySize != -1 { + buf.WriteString(fmt.Sprintf("%sKey Size: %d\n", prefix, auths.KeySize)) + } + if len(auths.BlockMode) > 0 { + buf.WriteString(fmt.Sprintf("%sBlockMode: %s\n", prefix, enumsToString(auths.BlockMode, blockModeToString))) + } + if len(auths.Digest) > 0 { + buf.WriteString(fmt.Sprintf("%sDigest: %s\n", prefix, enumsToString(auths.Digest, digestToString))) + } + if len(auths.Padding) > 0 { + buf.WriteString(fmt.Sprintf("%sPadding: %s\n", prefix, enumsToString(auths.Padding, paddingToString))) + } + showNullValue(buf, prefix, "Caller Nonce", auths.CallerNonce) + if auths.MinMacLength != -1 { + buf.WriteString(fmt.Sprintf("%sMin MAC Length: %d\n", prefix, auths.MinMacLength)) + } + if auths.EcCurve != -1 { + buf.WriteString(fmt.Sprintf("%sEcCurve: %s\n", prefix, ecCurveToString(auths.EcCurve))) + } + if auths.RsaPublicExponent != -1 { + buf.WriteString(fmt.Sprintf("%sRsa Public Exponent: %d\n", prefix, auths.RsaPublicExponent)) + } + if len(auths.MgfDigest) > 0 { + buf.WriteString(fmt.Sprintf("%sMGF Digest: %s\n", prefix, enumsToString(auths.MgfDigest, digestToString))) + } + showNullValue(buf, prefix, "Rollback Resistance", auths.RollbackResistance) + showNullValue(buf, prefix, "Early Boot Only", auths.EarlyBootOnly) + showDateTimeValue(buf, prefix, "Active Date Time", auths.ActiveDateTime) + showDateTimeValue(buf, prefix, "Origination Expire Date Time", auths.OriginationExpireDateTime) + showDateTimeValue(buf, prefix, "Usage Expire Date Time", auths.UsageExpireDateTime) + if auths.UsageCountLimit != -1 { + buf.WriteString(fmt.Sprintf("%sUsage Count Limit: %d\n", prefix, auths.UsageCountLimit)) + } + if auths.UserSecureId != -1 { + buf.WriteString(fmt.Sprintf("%sUser Secure ID: %d\n", prefix, auths.UserSecureId)) + } + showNullValue(buf, prefix, "No Auth Required", auths.NoAuthRequired) + if auths.UserAuthType != -1 { + buf.WriteString(fmt.Sprintf("%sUser Auth Type: 0x%02x\n", prefix, auths.UserAuthType)) + } + if auths.AuthTimeout != -1 { + buf.WriteString(fmt.Sprintf("%sAuth Timeout: %d\n", prefix, auths.AuthTimeout)) + } + showNullValue(buf, prefix, "Allow While On Body", auths.AllowWhileOnBody) + showNullValue(buf, prefix, "Trusted User Presence Req", auths.TrustedUserPresenceReq) + showNullValue(buf, prefix, "Trusted Confirmation Req", auths.TrustedConfirmationReq) + showNullValue(buf, prefix, "Unlock Device Req", auths.UnlockDeviceReq) + showDateTimeValue(buf, prefix, "Creation Date Time", auths.CreationDateTime) + if auths.Origin != -1 { + buf.WriteString(fmt.Sprintf("%sOrigin: %s\n", prefix, originToString(auths.Origin))) + } + + if len(auths.RootOfTrust.FullBytes) > 0 { + var rootOfTrust RootOfTrust + rest, err := asn1.Unmarshal(auths.RootOfTrust.Bytes, &rootOfTrust) + if err != nil { + buf.WriteString(fmt.Sprintf("%sRoot of Trust: FAILED TO PARSE %s\n", prefix, err)) + } else if len(rest) > 0 { + buf.WriteString(fmt.Sprintf("%sRoot of Trust: FAILURE: TRAILING DATA\n", prefix)) + } else { + buf.WriteString(fmt.Sprintf("%sRoot of Trust:\n", prefix)) + showHex(buf, prefix+" ", "Verified Boot Key", rootOfTrust.VerifiedBootKey) + buf.WriteString(fmt.Sprintf("%sDevice Locked: %v\n", prefix+" ", rootOfTrust.DeviceLocked)) + buf.WriteString(fmt.Sprintf("%sVerified Boot State: %s\n", prefix+" ", bootStateToString(rootOfTrust.VerifiedBootState))) + showHex(buf, prefix+" ", "Verified Boot Hash", rootOfTrust.VerifiedBootHash) + } + } + + if auths.OsVersion != -1 { + buf.WriteString(fmt.Sprintf("%sOS Version: %d\n", prefix, auths.OsVersion)) + } + if auths.OsPatchlevel != -1 { + buf.WriteString(fmt.Sprintf("%sOS Patchlevel: %d\n", prefix, auths.OsPatchlevel)) + } + + showOptionalAttestationAppId(buf, prefix, auths.AttestationApplicationId) + showOptionalHexUtf8(buf, prefix, "Attestation Id Brand", auths.AttestationIdBrand) + showOptionalHexUtf8(buf, prefix, "Attestation Id Device", auths.AttestationIdDevice) + showOptionalHexUtf8(buf, prefix, "Attestation Id Product", auths.AttestationIdProduct) + showOptionalHexUtf8(buf, prefix, "Attestation Id Serial", auths.AttestationIdSerial) + showOptionalHexUtf8(buf, prefix, "Attestation Id IMEI", auths.AttestationIdImei) + showOptionalHexUtf8(buf, prefix, "Attestation Id MEID", auths.AttestationIdMeid) + showOptionalHexUtf8(buf, prefix, "Attestation Id Manufacturer", auths.AttestationIdManufacturer) + showOptionalHexUtf8(buf, prefix, "Attestation Id Model", auths.AttestationIdModel) + if auths.VendorPatchlevel != -1 { + buf.WriteString(fmt.Sprintf("%sVendor Patchlevel: %d\n", prefix, auths.VendorPatchlevel)) + } + if auths.BootPatchlevel != -1 { + buf.WriteString(fmt.Sprintf("%sBoot Patchlevel: %d\n", prefix, auths.BootPatchlevel)) + } + showNullValue(buf, prefix, "Device Unique Attestation", auths.DeviceUniqueAttestation) + showNullValue(buf, prefix, "Identity Credential Key", auths.IdentityCredentialKey) + showOptionalHexUtf8(buf, prefix, "Attestation Id Second IMEI", auths.AttestationIdSecondImei) +} + +func enumsToString(vals []int, fp func(int) string) string { + var buf bytes.Buffer + for ii, v := range vals { + if ii > 0 { + buf.WriteString(", ") + } + buf.WriteString(fp(v)) + } + return buf.String() +} + +func purposeToString(v int) string { + switch v { + case 0: + return "ENCRYPT" + case 1: + return "DECRYPT" + case 2: + return "SIGN" + case 3: + return "VERIFY" + case 5: + return "WRAP_KEY" + case 6: + return "AGREE_KEY" + case 7: + return "ATTEST_KEY" + } + return fmt.Sprintf("UNKNOWN(%d)", v) +} + +func algorithmToString(v int) string { + switch v { + case 1: + return "RSA" + case 3: + return "EC" + case 32: + return "AES" + case 33: + return "TRIPLE_DES" + case 128: + return "HMAC" + } + return fmt.Sprintf("UNKNOWN(%d)", v) + +} + +func digestToString(v int) string { + switch v { + case 0: + return "NONE" + case 1: + return "MD5" + case 2: + return "SHA1" + case 3: + return "SHA_2_224" + case 4: + return "SHA_2_256" + case 5: + return "SHA_2_384" + case 6: + return "SHA_2_512" + } + return fmt.Sprintf("UNKNOWN(%d)", v) +} + +func paddingToString(v int) string { + switch v { + case 1: + return "NONE" + case 2: + return "RSA_OAEP" + case 3: + return "RSA_PSS" + case 4: + return "RSA_PKCS1_1_5_ENCRYPT" + case 5: + return "RSA_PKCS1_1_5_SIGN" + case 64: + return "PKCS7" + } + return fmt.Sprintf("UNKNOWN(%d)", v) +} + +func blockModeToString(v int) string { + switch v { + case 1: + return "ECB" + case 2: + return "CBC" + case 3: + return "CTR" + case 32: + return "GCM" + } + return fmt.Sprintf("UNKNOWN(%d)", v) +} + +func ecCurveToString(v int) string { + switch v { + case 0: + return "P_224" + case 1: + return "P_256" + case 2: + return "P_384" + case 3: + return "P_521" + case 4: + return "CURVE_25519" + } + return fmt.Sprintf("UNKNOWN(%d)", v) +} + +func originToString(v int) string { + switch v { + case 0: + return "GENERATED" + case 1: + return "DERIVED" + case 2: + return "IMPORTED" + case 3: + return "RESERVED" + case 4: + return "SECURELY_IMPORTED" + } + return fmt.Sprintf("UNKNOWN(%d)", v) +} + +func showNullValue(buf *bytes.Buffer, prefix string, name string, val asn1.RawValue) { + if bytes.Equal(val.Bytes, asn1.NullBytes) { + buf.WriteString(fmt.Sprintf("%s%s: TRUE\n", prefix, name)) + } else if len(val.FullBytes) == 0 { + // Absent => FALSE + } else { + buf.WriteString(fmt.Sprintf("%s%s: invalid contents! %+v\n", prefix, name, val)) + } +} + +func showDateTimeValue(buf *bytes.Buffer, prefix string, name string, val int64) { + if val == -1 { + return + } + // Value is milliseconds since epoch + timestamp := time.Unix(val/1000, (val%1000)*1000000) + buf.WriteString(fmt.Sprintf("%s%s: %s\n", prefix, name, timestamp)) +} + +func showOptionalHex(buf *bytes.Buffer, prefix string, name string, val []byte) { + if len(val) == 0 { + return + } + showHex(buf, prefix, name, val) +} + +func showOptionalHexUtf8(buf *bytes.Buffer, prefix string, name string, val []byte) { + if len(val) == 0 { + return + } + showHexUtf8(buf, prefix, name, val) +} + +func showHex(buf *bytes.Buffer, prefix string, name string, val []byte) { + buf.WriteString(fmt.Sprintf("%s%s:\n", prefix, name)) + appendHexData(buf, val, 64, prefix+" ") + buf.WriteString("\n") +} + +func showHexUtf8(buf *bytes.Buffer, prefix string, name string, val []byte) { + buf.WriteString(fmt.Sprintf("%s%s:\n", prefix, name)) + appendHexData(buf, val, 64, prefix+" ") + // The hex data might also be a valid string. + if utf8.Valid(val) { + buf.WriteString(fmt.Sprintf(" == '%s'", string(val))) + } + + buf.WriteString("\n") +} + +// AndroidAttestationAppId describes an Android application identifier. +type AndroidAttestationAppId struct { + PackageInfoRecords []AndroidPackageInfoRecord `asn1:"set"` + SignatureDigests [][]byte `asn1:"set"` +} + +// AndroidPackageInfoRecord hold a package info record from Android. +type AndroidPackageInfoRecord struct { + PackageName []byte + Version int +} + +func AndroidAppInfoFromData(val []byte) (*AndroidAttestationAppId, error) { + var appId AndroidAttestationAppId + rest, err := asn1.Unmarshal(val, &appId) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal attestation app ID: %v", err) + } else if len(rest) > 0 { + return nil, fmt.Errorf("trailing data (%d bytes) after attestation app ID", len(rest)) + } + return &appId, nil +} + +func showOptionalAttestationAppId(buf *bytes.Buffer, prefix string, val []byte) { + if len(val) == 0 { + return + } + buf.WriteString(fmt.Sprintf("%sAttestation Application Id:\n", prefix)) + // Attempt to parse as app ID + appId, err := AndroidAppInfoFromData(val) + if err == nil { + for _, pkg := range appId.PackageInfoRecords { + buf.WriteString(fmt.Sprintf("%s Name:%q Version:%d\n", prefix, pkg.PackageName, pkg.Version)) + } + for _, sig := range appId.SignatureDigests { + buf.WriteString(fmt.Sprintf("%s Signature: %x", prefix, sig)) + } + } else { + appendHexData(buf, val, 64, prefix+" ") + } + buf.WriteString("\n") +} diff --git a/x509util/certcheck/certcheck.go b/x509util/certcheck/certcheck.go index f9aee630c3..790e400b28 100644 --- a/x509util/certcheck/certcheck.go +++ b/x509util/certcheck/certcheck.go @@ -20,6 +20,7 @@ import ( "crypto/tls" "flag" "fmt" + "io/ioutil" "net/url" "os" "strings" @@ -35,7 +36,7 @@ var ( useSystemRoots = flag.Bool("system_roots", false, "Use system roots") verbose = flag.Bool("verbose", false, "Verbose output") strict = flag.Bool("strict", true, "Set non-zero exit code for non-fatal errors in parsing") - validate = flag.Bool("validate", false, "Validate certificate signatures") + validate = flag.Bool("validate", false, "Validate certificate signatures (leaf to root)") checkTime = flag.Bool("check_time", false, "Check current validity of certificate") checkName = flag.Bool("check_name", true, "Check certificate name validity") checkEKU = flag.Bool("check_eku", true, "Check EKU nesting validity") @@ -45,7 +46,7 @@ var ( checkRevoked = flag.Bool("check_revocation", false, "Check revocation status of certificate") ) -func addCerts(filename string, pool *x509.CertPool) { +func addCerts(filename string, pool *x509.CertPool, validateSelfSigned bool) { if filename != "" { dataList, err := x509util.ReadPossiblePEMFile(filename, "CERTIFICATE") if err != nil { @@ -57,6 +58,12 @@ func addCerts(filename string, pool *x509.CertPool) { glog.Exitf("Failed to parse certificate from %s: %v", filename, err) } for _, cert := range certs { + if validateSelfSigned { + err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature) + if err != nil { + glog.Exitf("Failed to verify self-signature on root cert from %s: %v", filename, err) + } + } pool.AddCert(cert) } } @@ -66,12 +73,19 @@ func addCerts(filename string, pool *x509.CertPool) { func main() { flag.Parse() + targets := flag.Args() + if len(targets) == 0 { + // No arguments specific, use special value to trigger read from stdin. + targets = []string{"--"} + } failed := false - for _, target := range flag.Args() { + for _, target := range targets { var err error var chain []*x509.Certificate if strings.HasPrefix(target, "https://") { chain, err = chainFromSite(target) + } else if target == "--" { + chain, err = chainFromStdin() } else { chain, err = chainFromFile(target) } @@ -97,6 +111,7 @@ func main() { } if *validate && len(chain) > 0 { opts := x509.VerifyOptions{ + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, DisableTimeChecks: !*checkTime, DisableCriticalExtensionChecks: !*checkUnknownCriticalExts, DisableNameChecks: !*checkName, @@ -167,16 +182,33 @@ func chainFromFile(filename string) ([]*x509.Certificate, error) { if err != nil { return nil, fmt.Errorf("%s: failed to read data: %v", filename, err) } + return chainFromData(dataList, filename) +} + +// chainFromStdin retrieves a certificate chain from stdin. +// Note that both a chain and an error can be returned (in which case +// the error will be of type x509.NonFatalErrors). +func chainFromStdin() ([]*x509.Certificate, error) { + data, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return nil, fmt.Errorf(": failed to read data: %v", err) + } + + dataList := x509util.DePEM(data, "CERTIFICATE") + return chainFromData(dataList, "") +} + +func chainFromData(dataList [][]byte, source string) ([]*x509.Certificate, error) { var nfe *x509.NonFatalErrors var chain []*x509.Certificate for _, data := range dataList { certs, err := x509.ParseCertificates(data) if x509.IsFatal(err) { - return nil, fmt.Errorf("%s: failed to parse: %v", filename, err) + return nil, fmt.Errorf("%s: failed to parse: %v", source, err) } else if errs, ok := err.(x509.NonFatalErrors); ok { nfe = nfe.Append(&errs) } else if err != nil { - return nil, fmt.Errorf("%s: failed to parse: %v", filename, err) + return nil, fmt.Errorf("%s: failed to parse: %v", source, err) } chain = append(chain, certs...) } @@ -195,11 +227,10 @@ func validateChain(chain []*x509.Certificate, opts x509.VerifyOptions, rootsFile } roots = systemRoots } - opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageAny} opts.Roots = roots opts.Intermediates = x509.NewCertPool() - addCerts(rootsFile, opts.Roots) - addCerts(intermediatesFile, opts.Intermediates) + addCerts(rootsFile, opts.Roots /* validate_self_signed= */, true) + addCerts(intermediatesFile, opts.Intermediates /* validate_self_signed= */, false) if !useSystemRoots && len(rootsFile) == 0 { // No root CA certs provided, so assume the chain is self-contained. @@ -216,6 +247,32 @@ func validateChain(chain []*x509.Certificate, opts x509.VerifyOptions, rootsFile opts.Intermediates.AddCert(chain[i]) } } + + failed := false + for i, cert := range chain { + var msg string + var signer *x509.Certificate + if i == len(chain)-1 { + signer = cert + msg = "self-signature" + } else { + signer = chain[i+1] + msg = fmt.Sprintf("signature from cert [%d]", i+1) + } + + err := signer.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature) + if err != nil { + glog.Errorf("Failed to verify %s on certificate [%d]: %v", msg, i, err) + failed = true + } else if *verbose { + fmt.Printf("Certificate [%d] with subject %q has valid %s\n", i, cert.Subject, msg) + } + } + if failed { + glog.Exitf("Signature verification failed") + } + + // Also do full x509 verification according to the options. _, err := chain[0].Verify(opts) return err } diff --git a/x509util/files.go b/x509util/files.go index 70b6a2380d..9e799e15fc 100644 --- a/x509util/files.go +++ b/x509util/files.go @@ -32,7 +32,7 @@ func ReadPossiblePEMFile(filename, blockname string) ([][]byte, error) { if err != nil { return nil, fmt.Errorf("%s: failed to read data: %v", filename, err) } - return dePEM(data, blockname), nil + return DePEM(data, blockname), nil } // ReadPossiblePEMURL attempts to determine if the given target is a local file or a @@ -52,10 +52,12 @@ func ReadPossiblePEMURL(target, blockname string) ([][]byte, error) { if err != nil { return nil, fmt.Errorf("failed to ioutil.ReadAll(%q): %v", target, err) } - return dePEM(data, blockname), nil + return DePEM(data, blockname), nil } -func dePEM(data []byte, blockname string) [][]byte { +// DePEM converts PEM-encoded data with the given blockname to binary data, allowing +// for multiple PEM blocks. +func DePEM(data []byte, blockname string) [][]byte { var results [][]byte if strings.Contains(string(data), "BEGIN "+blockname) { rest := data diff --git a/x509util/x509util.go b/x509util/x509util.go index c5c9d24822..0613a73e83 100644 --- a/x509util/x509util.go +++ b/x509util/x509util.go @@ -20,8 +20,10 @@ import ( "bytes" "crypto/dsa" "crypto/ecdsa" + "crypto/ed25519" "crypto/elliptic" "crypto/rsa" + "crypto/sha1" "encoding/base64" "encoding/hex" "encoding/pem" @@ -84,6 +86,28 @@ func OIDInExtensions(oid asn1.ObjectIdentifier, extensions []pkix.Extension) (in return count, critical } +type publicKeyInfo struct { + Raw asn1.RawContent + Algorithm pkix.AlgorithmIdentifier + PublicKey asn1.BitString +} + +// Calculate the key ID from the subject public key of the certificate, according to the +// section 4.2.1.2 of RFC 5280: +// - parse the SubjectPublicKey into SEQUENCE { AlgorithmIdentifier, BIT STRING} +// - extract the contents of the BIT STRING (no tag/len/unused-bit-count) +// - SHA-1 hash the result +func CalculateSubjectKeyId(cert *x509.Certificate) ([]byte, error) { + var pki publicKeyInfo + if rest, err := asn1.Unmarshal(cert.RawSubjectPublicKeyInfo, &pki); err != nil { + return nil, err + } else if len(rest) != 0 { + return nil, errors.New("x509: trailing data after ASN.1 of public-key") + } + hash := sha1.Sum(pki.PublicKey.Bytes) + return hash[:], nil +} + // String formatting for various X.509/ASN.1 types func bitStringToString(b asn1.BitString) string { // nolint:deadcode,unused result := hex.EncodeToString(b.Bytes) @@ -103,6 +127,10 @@ func publicKeyAlgorithmToString(algo x509.PublicKeyAlgorithm) string { return "dsaEncryption" case x509.ECDSA: return "id-ecPublicKey" + case x509.Ed25519: + return "Ed25519" + case x509.X25519: + return "X25519" default: return strconv.Itoa(int(algo)) } @@ -174,8 +202,14 @@ func publicKeyToString(_ x509.PublicKeyAlgorithm, pub interface{}) string { appendHexData(&buf, data, 15, " ") buf.WriteString("\n") buf.WriteString(fmt.Sprintf(" ASN1 OID: %s", oidname)) + case ed25519.PublicKey: + buf.WriteString(" pub:\n") + appendHexData(&buf, []byte(pub), 15, " ") + case []byte: + buf.WriteString(" pub:\n") + appendHexData(&buf, pub, 15, " ") default: - buf.WriteString(fmt.Sprintf("%v", pub)) + buf.WriteString(fmt.Sprintf(" %T: %v", pub, pub)) } return buf.String() } @@ -436,6 +470,9 @@ func CertificateToString(cert *x509.Certificate) string { showCTPoison(&result, cert) showCTSCT(&result, cert) showCTLogSTHInfo(&result, cert) + showAndroidAttestation(&result, cert) + showAndroidRkpInfo(&result, cert) + showAndroidVmAttestation(&result, cert) showUnhandledExtensions(&result, cert) showSignature(&result, cert) @@ -465,6 +502,11 @@ func showSubjectKeyID(result *bytes.Buffer, cert *x509.Certificate) { result.WriteString(fmt.Sprintf(" X509v3 Subject Key Identifier:")) showCritical(result, critical) result.WriteString(fmt.Sprintf(" keyid:%v\n", hex.EncodeToString(cert.SubjectKeyId))) + + calcKeyId, err := CalculateSubjectKeyId(cert) + if err == nil && !bytes.Equal(calcKeyId, cert.SubjectKeyId) { + result.WriteString(fmt.Sprintf(" NOTE: keyid != calculated value %v\n", hex.EncodeToString(calcKeyId))) + } } } @@ -789,7 +831,10 @@ func oidAlreadyPrinted(oid asn1.ObjectIdentifier) bool { oid.Equal(x509.OIDExtensionASList) || oid.Equal(x509.OIDExtensionCTPoison) || oid.Equal(x509.OIDExtensionCTSCT) || - oid.Equal(x509ext.OIDExtensionCTSTH) { + oid.Equal(x509ext.OIDExtensionCTSTH) || + oid.Equal(OIDExtensionAndroidAttestation) || + oid.Equal(OIDExtensionAndroidVmAttestation) || + oid.Equal(OIDExtensionAndroidRkpInfo) { return true } return false