diff --git a/attest/activation.go b/attest/activation.go index 0cd884b..917cbda 100644 --- a/attest/activation.go +++ b/attest/activation.go @@ -3,6 +3,7 @@ package attest import ( "bytes" "crypto" + "crypto/ecdsa" "crypto/rand" "crypto/rsa" "errors" @@ -20,6 +21,8 @@ import ( const ( // minRSABits is the minimum accepted bit size of an RSA key. minRSABits = 2048 + // minECCBits is the minimum accepted bit size of an ECC key. + minECCBits = 256 // activationSecretLen is the size in bytes of the generated secret // which is generated for credential activation. activationSecretLen = 32 @@ -115,6 +118,12 @@ func (p *ActivationParameters) checkTPM20AKParameters() error { if pub.RSAParameters.KeyBits < minRSABits { return fmt.Errorf("attestation key too small: must be at least %d bits but was %d bits", minRSABits, pub.RSAParameters.KeyBits) } + case tpm2.AlgECC: + if len(pub.ECCParameters.Point.XRaw)*8 < minECCBits { + return fmt.Errorf("attestation key too small: must be at least %d bits but was %d bits", minECCBits, len(pub.ECCParameters.Point.XRaw)*8) + } else if len(pub.ECCParameters.Point.YRaw)*8 < minECCBits { + return fmt.Errorf("attestation key too small: must be at least %d bits but was %d bits", minECCBits, len(pub.ECCParameters.Point.YRaw)*8) + } default: return fmt.Errorf("public key of alg 0x%x not supported", pub.Type) } @@ -160,6 +169,17 @@ func (p *ActivationParameters) checkTPM20AKParameters() error { } // Check the signature over the attestation data verifies correctly. + switch pub.Type { + case tpm2.AlgRSA: + return verifyRSASignature(pub, p) + case tpm2.AlgECC: + return verifyECDSASignature(pub, p) + default: + return fmt.Errorf("public key of alg 0x%x not supported", pub.Type) + } +} + +func verifyRSASignature(pub tpm2.Public, p *ActivationParameters) error { pk := rsa.PublicKey{E: int(pub.RSAParameters.Exponent()), N: pub.RSAParameters.Modulus()} signHash, err := pub.RSAParameters.Sign.Hash.Hash() if err != nil { @@ -188,6 +208,40 @@ func (p *ActivationParameters) checkTPM20AKParameters() error { return nil } +func verifyECDSASignature(pub tpm2.Public, p *ActivationParameters) error { + key, err := pub.Key() + if err != nil { + return nil + } + pk, ok := key.(*ecdsa.PublicKey) + if !ok { + return fmt.Errorf("expected *ecdsa.PublicKey, got %T", key) + } + signHash, err := pub.ECCParameters.Sign.Hash.Hash() + if err != nil { + return err + } + hsh := signHash.New() + _, err = hsh.Write(p.AK.CreateAttestation) + if err != nil { + return err + } + + if len(p.AK.CreateSignature) < 8 { + return fmt.Errorf("signature invalid: length of %d is shorter than 8", len(p.AK.CreateSignature)) + } + + sig, err := tpm2.DecodeSignature(bytes.NewBuffer(p.AK.CreateSignature)) + if err != nil { + return fmt.Errorf("DecodeSignature() failed: %v", err) + } + + if !ecdsa.Verify(pk, hsh.Sum(nil), sig.ECC.R, sig.ECC.S) { + return fmt.Errorf("unable to verify attestation for ecdsa credential") + } + return nil +} + // Generate returns a credential activation challenge, which can be provided // to the TPM to verify the AK parameters given are authentic & the AK // is present on the same TPM as the EK. diff --git a/attest/activation_test.go b/attest/activation_test.go index 16e2586..76574f5 100644 --- a/attest/activation_test.go +++ b/attest/activation_test.go @@ -67,3 +67,33 @@ func TestActivationTPM20(t *testing.T) { t.Fatalf("secret = %v, want %v", got, want) } } + +func TestECCActivationTPM20(t *testing.T) { + priv := ekCertSigner(t) + rand := rand.New(rand.NewSource(123456)) + + // These parameters represent an ECC P256 AK generated on a real-world, + // Google vTPM. + params := ActivationParameters{ + TPMVersion: TPMVersion20, + AK: AttestationParameters{ + Public: decodeBase64("ACMACwAFBHIAIJ3/y/NsODrmmfuYaNxty4nXFTiEvigDkiwSQVi/rSKuABAAGAAEAAMAEAAgydcfFWW6O7PjW9vnOE4IDx3545TxylD1iVHP8MIFI78AIJuD/QM9EbqM+3SEl7PgiXlWV1NhmvnmE2AHiEfI/hUn", t), + CreateData: decodeBase64("AAAAAAAg47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUBAAsAIgALLEyPGewwEIKqPNw9Lx7QXsfp0MsOZFt4EzHFT4tSXekAIgALf7O+OxqTNzTuIhi1YGQoulPZoyxsNKHpBgT4dHqT3+UAAA==", t), + CreateAttestation: decodeBase64("/1RDR4AaACIAC8Uqr5OAkfcyLJ2gLU2oSvIoPi8XdoLHGdpS5h/JJz8aAAAAAAAEXezQqarG49k44bBnAUna81DZr1xhACIAC3AoqIzDrusUtdH3uAwqbqUrybtu35H1XPQDyLBHVGF+ACCKEKpwL1LFbw/IG+vtJ6CtXHIYBhVWyrkAqYLkHleYMw==", t), + CreateSignature: decodeBase64("ABgABAAgbTEBcZvjb9uEEYZCiSqPh/XO0BZQS+egnJ8WKtpSbmkAIDNgvF9iyiOCvd5480hOCzjRj7GP3YZ3XqjEVvN3Q3Ca", t), + }, + EK: &rsa.PublicKey{ + E: priv.E, + N: priv.N, + }, + Rand: rand, + } + + secret, _, err := params.Generate() + if err != nil { + t.Fatalf("Generate() returned err: %v", err) + } + if got, want := secret, decodeBase64("0vhS7HtORX9uf/iyQ8Sf9WkpJuoJ1olCfTjSZuyNNxY=", t); !bytes.Equal(got, want) { + t.Fatalf("secret = %v, want %v", got, want) + } +} diff --git a/attest/application_key.go b/attest/application_key.go index 2e0ba6d..e7f19dc 100644 --- a/attest/application_key.go +++ b/attest/application_key.go @@ -63,6 +63,13 @@ type Algorithm string const ( ECDSA Algorithm = "ECDSA" RSA Algorithm = "RSA" + + // Windows specific ECDSA CNG algorithm identifiers. + // NOTE: Using ECDSA will default to ECDSA_P256. + // Ref: https://learn.microsoft.com/en-us/windows/win32/SecCNG/cng-algorithm-identifiers + ECDSA_P256 Algorithm = "ECDSA_P256" + ECDSA_P384 Algorithm = "ECDSA_P384" + ECDSA_P521 Algorithm = "ECDSA_P521" ) // KeyConfig encapsulates parameters for minting keys. @@ -88,6 +95,24 @@ var defaultConfig = &KeyConfig{ Size: 256, } +// Size returns the bit size associated with an algorithm. +func (a Algorithm) Size() int { + switch a { + case RSA: + return 2048 + case ECDSA: + return 256 + case ECDSA_P256: + return 256 + case ECDSA_P384: + return 384 + case ECDSA_P521: + return 521 + default: + return 0 + } +} + // Public returns the public key corresponding to the private key. func (k *Key) Public() crypto.PublicKey { return k.pub diff --git a/attest/attest.go b/attest/attest.go index e55d382..402342b 100644 --- a/attest/attest.go +++ b/attest/attest.go @@ -190,6 +190,8 @@ func (k *AK) Certify(tpm *TPM, handle interface{}) (*CertificationParameters, er // AKConfig encapsulates parameters for minting keys. type AKConfig struct { + // Optionally set unique name for AK on Windows. + Name string // Parent describes the Storage Root Key that will be used as a parent. // If nil, the default SRK (i.e. RSA with handle 0x81000001) is assumed. // Supported only by TPM 2.0 on Linux. diff --git a/attest/attest_test.go b/attest/attest_test.go index 43ca9a1..9343fc1 100644 --- a/attest/attest_test.go +++ b/attest/attest_test.go @@ -131,12 +131,13 @@ func TestAKCreateAndLoad(t *testing.T) { } defer loaded.Close(tpm) - k1, k2 := ak.ak.(*wrappedKey20), loaded.ak.(*wrappedKey20) + k1 := ak.ak.attestationParameters() + k2 := loaded.ak.attestationParameters() - if !bytes.Equal(k1.public, k2.public) { + if !bytes.Equal(k1.Public, k2.Public) { t.Error("Original & loaded AK public blobs did not match.") - t.Logf("Original = %v", k1.public) - t.Logf("Loaded = %v", k2.public) + t.Logf("Original = %v", k1.Public) + t.Logf("Loaded = %v", k2.Public) } }) } diff --git a/attest/pcp_windows.go b/attest/pcp_windows.go index b78a381..1b6d2f5 100644 --- a/attest/pcp_windows.go +++ b/attest/pcp_windows.go @@ -453,19 +453,19 @@ func getPCPCerts(hProv uintptr, propertyName string) ([][]byte, error) { } // NewAK creates a persistent attestation key of the specified name. -func (h *winPCP) NewAK(name string) (uintptr, error) { +func (h *winPCP) NewAK(name string, alg Algorithm) (uintptr, error) { var kh uintptr utf16Name, err := windows.UTF16FromString(name) if err != nil { return 0, err } - utf16RSA, err := windows.UTF16FromString("RSA") + utf16Alg, err := windows.UTF16FromString(string(alg)) if err != nil { return 0, err } // Create a persistent RSA key of the specified name. - r, _, msg := nCryptCreatePersistedKey.Call(h.hProv, uintptr(unsafe.Pointer(&kh)), uintptr(unsafe.Pointer(&utf16RSA[0])), uintptr(unsafe.Pointer(&utf16Name[0])), 0, 0) + r, _, msg := nCryptCreatePersistedKey.Call(h.hProv, uintptr(unsafe.Pointer(&kh)), uintptr(unsafe.Pointer(&utf16Alg[0])), uintptr(unsafe.Pointer(&utf16Name[0])), 0, 0) if r != 0 { if tpmErr := maybeWinErr(r); tpmErr != nil { msg = tpmErr @@ -477,7 +477,7 @@ func (h *winPCP) NewAK(name string) (uintptr, error) { if err != nil { return 0, err } - var length uint32 = 2048 + var length uint32 = uint32(alg.Size()) r, _, msg = nCryptSetProperty.Call(kh, uintptr(unsafe.Pointer(&utf16Length[0])), uintptr(unsafe.Pointer(&length)), unsafe.Sizeof(length), 0) if r != 0 { if tpmErr := maybeWinErr(r); tpmErr != nil { diff --git a/attest/tpm_windows.go b/attest/tpm_windows.go index e1daeae..1a1a121 100644 --- a/attest/tpm_windows.go +++ b/attest/tpm_windows.go @@ -287,13 +287,26 @@ func decryptCredential(secretKey, blob []byte) ([]byte, error) { } func (t *windowsTPM) newAK(opts *AKConfig) (*AK, error) { - nameHex := make([]byte, 5) - if n, err := rand.Read(nameHex); err != nil || n != len(nameHex) { - return nil, fmt.Errorf("rand.Read() failed with %d/%d bytes read and error: %v", n, len(nameHex), err) - } - name := fmt.Sprintf("ak-%x", nameHex) + var name string + var alg Algorithm - kh, err := t.pcp.NewAK(name) + if opts != nil && opts.Name != "" { + name = opts.Name + } else { + nameHex := make([]byte, 5) + if n, err := rand.Read(nameHex); err != nil || n != len(nameHex) { + return nil, fmt.Errorf("rand.Read() failed with %d/%d bytes read and error: %v", n, len(nameHex), err) + } + name = fmt.Sprintf("ak-%x", nameHex) + } + if opts != nil && opts.Algorithm != "" { + alg = opts.Algorithm + } else { + // Default to RSA based AK. + alg = RSA + } + + kh, err := t.pcp.NewAK(name, alg) if err != nil { return nil, fmt.Errorf("pcp failed to mint attestation key: %v", err) }