Support webauthn (#17957)

Migrate from U2F to Webauthn

Co-authored-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Lunny Xiao
2022-01-14 16:03:31 +01:00
committed by GitHub
co-authored by Andrew Thornton 6543 wxiaoguang
parent 8808293247
commit 35c3553870
224 changed files with 35040 additions and 1079 deletions
+155
View File
@@ -0,0 +1,155 @@
package protocol
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/duo-labs/webauthn/protocol/webauthncose"
)
// The raw response returned to us from an authenticator when we request a
// credential for login/assertion.
type CredentialAssertionResponse struct {
PublicKeyCredential
AssertionResponse AuthenticatorAssertionResponse `json:"response"`
}
// The parsed CredentialAssertionResponse that has been marshalled into a format
// that allows us to verify the client and authenticator data inside the response
type ParsedCredentialAssertionData struct {
ParsedPublicKeyCredential
Response ParsedAssertionResponse
Raw CredentialAssertionResponse
}
// The AuthenticatorAssertionResponse contains the raw authenticator assertion data and is parsed into
// ParsedAssertionResponse
type AuthenticatorAssertionResponse struct {
AuthenticatorResponse
AuthenticatorData URLEncodedBase64 `json:"authenticatorData"`
Signature URLEncodedBase64 `json:"signature"`
UserHandle URLEncodedBase64 `json:"userHandle,omitempty"`
}
// Parsed form of AuthenticatorAssertionResponse
type ParsedAssertionResponse struct {
CollectedClientData CollectedClientData
AuthenticatorData AuthenticatorData
Signature []byte
UserHandle []byte
}
// Parse the credential request response into a format that is either required by the specification
// or makes the assertion verification steps easier to complete. This takes an http.Request that contains
// the assertion response data in a raw, mostly base64 encoded format, and parses the data into
// manageable structures
func ParseCredentialRequestResponse(response *http.Request) (*ParsedCredentialAssertionData, error) {
if response == nil || response.Body == nil {
return nil, ErrBadRequest.WithDetails("No response given")
}
return ParseCredentialRequestResponseBody(response.Body)
}
// Parse the credential request response into a format that is either required by the specification
// or makes the assertion verification steps easier to complete. This takes an io.Reader that contains
// the assertion response data in a raw, mostly base64 encoded format, and parses the data into
// manageable structures
func ParseCredentialRequestResponseBody(body io.Reader) (*ParsedCredentialAssertionData, error) {
var car CredentialAssertionResponse
err := json.NewDecoder(body).Decode(&car)
if err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Assertion")
}
if car.ID == "" {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID missing")
}
_, err = base64.RawURLEncoding.DecodeString(car.ID)
if err != nil {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID not base64url encoded")
}
if car.Type != "public-key" {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with bad type")
}
var par ParsedCredentialAssertionData
par.ID, par.RawID, par.Type, par.ClientExtensionResults = car.ID, car.RawID, car.Type, car.ClientExtensionResults
par.Raw = car
par.Response.Signature = car.AssertionResponse.Signature
par.Response.UserHandle = car.AssertionResponse.UserHandle
// Step 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
// We don't call it cData but this is Step 5 in the spec.
err = json.Unmarshal(car.AssertionResponse.ClientDataJSON, &par.Response.CollectedClientData)
if err != nil {
return nil, err
}
err = par.Response.AuthenticatorData.Unmarshal(car.AssertionResponse.AuthenticatorData)
if err != nil {
return nil, ErrParsingData.WithDetails("Error unmarshalling auth data")
}
return &par, nil
}
// Follow the remaining steps outlined in §7.2 Verifying an authentication assertion
// (https://www.w3.org/TR/webauthn/#verifying-assertion) and return an error if there
// is a failure during each step.
func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID, relyingPartyOrigin, appID string, verifyUser bool, credentialBytes []byte) error {
// Steps 4 through 6 in verifying the assertion data (https://www.w3.org/TR/webauthn/#verifying-assertion) are
// "assertive" steps, i.e "Let JSONtext be the result of running UTF-8 decode on the value of cData."
// We handle these steps in part as we verify but also beforehand
// Handle steps 7 through 10 of assertion by verifying stored data against the Collected Client Data
// returned by the authenticator
validError := p.Response.CollectedClientData.Verify(storedChallenge, AssertCeremony, relyingPartyOrigin)
if validError != nil {
return validError
}
// Begin Step 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP.
rpIDHash := sha256.Sum256([]byte(relyingPartyID))
var appIDHash [32]byte
if appID != "" {
appIDHash = sha256.Sum256([]byte(appID))
}
// Handle steps 11 through 14, verifying the authenticator data.
validError = p.Response.AuthenticatorData.Verify(rpIDHash[:], appIDHash[:], verifyUser)
if validError != nil {
return ErrAuthData.WithInfo(validError.Error())
}
// allowedUserCredentialIDs := session.AllowedCredentialIDs
// Step 15. Let hash be the result of computing a hash over the cData using SHA-256.
clientDataHash := sha256.Sum256(p.Raw.AssertionResponse.ClientDataJSON)
// Step 16. Using the credential public key looked up in step 3, verify that sig is
// a valid signature over the binary concatenation of authData and hash.
sigData := append(p.Raw.AssertionResponse.AuthenticatorData, clientDataHash[:]...)
var (
key interface{}
err error
)
if appID == "" {
key, err = webauthncose.ParsePublicKey(credentialBytes)
} else {
key, err = webauthncose.ParseFIDOPublicKey(credentialBytes)
}
valid, err := webauthncose.VerifySignature(key, sigData, p.Response.Signature)
if !valid {
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error validating the assertion signature: %+v\n", err))
}
return nil
}
+156
View File
@@ -0,0 +1,156 @@
package protocol
import (
"crypto/sha256"
"encoding/json"
"fmt"
"github.com/fxamacker/cbor/v2"
)
// From §5.2.1 (https://www.w3.org/TR/webauthn/#authenticatorattestationresponse)
// "The authenticator's response to a clients request for the creation
// of a new public key credential. It contains information about the new credential
// that can be used to identify it for later use, and metadata that can be used by
// the WebAuthn Relying Party to assess the characteristics of the credential
// during registration."
// The initial unpacked 'response' object received by the relying party. This
// contains the clientDataJSON object, which will be marshalled into
// CollectedClientData, and the 'attestationObject', which contains
// information about the authenticator, and the newly minted
// public key credential. The information in both objects are used
// to verify the authenticity of the ceremony and new credential
type AuthenticatorAttestationResponse struct {
// The byte slice of clientDataJSON, which becomes CollectedClientData
AuthenticatorResponse
// The byte slice version of AttestationObject
// This attribute contains an attestation object, which is opaque to, and
// cryptographically protected against tampering by, the client. The
// attestation object contains both authenticator data and an attestation
// statement. The former contains the AAGUID, a unique credential ID, and
// the credential public key. The contents of the attestation statement are
// determined by the attestation statement format used by the authenticator.
// It also contains any additional information that the Relying Party's server
// requires to validate the attestation statement, as well as to decode and
// validate the authenticator data along with the JSON-serialized client data.
AttestationObject URLEncodedBase64 `json:"attestationObject"`
}
// The parsed out version of AuthenticatorAttestationResponse.
type ParsedAttestationResponse struct {
CollectedClientData CollectedClientData
AttestationObject AttestationObject
}
// From §6.4. Authenticators MUST also provide some form of attestation. The basic requirement is that the
// authenticator can produce, for each credential public key, an attestation statement verifiable by the
// WebAuthn Relying Party. Typically, this attestation statement contains a signature by an attestation
// private key over the attested credential public key and a challenge, as well as a certificate or similar
// data providing provenance information for the attestation public key, enabling the Relying Party to make
// a trust decision. However, if an attestation key pair is not available, then the authenticator MUST
// perform self attestation of the credential public key with the corresponding credential private key.
// All this information is returned by authenticators any time a new public key credential is generated, in
// the overall form of an attestation object. (https://www.w3.org/TR/webauthn/#attestation-object)
//
type AttestationObject struct {
// The authenticator data, including the newly created public key. See AuthenticatorData for more info
AuthData AuthenticatorData
// The byteform version of the authenticator data, used in part for signature validation
RawAuthData []byte `json:"authData"`
// The format of the Attestation data.
Format string `json:"fmt"`
// The attestation statement data sent back if attestation is requested.
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
}
type attestationFormatValidationHandler func(AttestationObject, []byte) (string, []interface{}, error)
var attestationRegistry = make(map[string]attestationFormatValidationHandler)
// Using one of the locally registered attestation formats, handle validating the attestation
// data provided by the authenticator (and in some cases its manufacturer)
func RegisterAttestationFormat(format string, handler attestationFormatValidationHandler) {
attestationRegistry[format] = handler
}
// Parse the values returned in the authenticator response and perform attestation verification
// Step 8. This returns a fully decoded struct with the data put into a format that can be
// used to verify the user and credential that was created
func (ccr *AuthenticatorAttestationResponse) Parse() (*ParsedAttestationResponse, error) {
var p ParsedAttestationResponse
err := json.Unmarshal(ccr.ClientDataJSON, &p.CollectedClientData)
if err != nil {
return nil, ErrParsingData.WithInfo(err.Error())
}
err = cbor.Unmarshal(ccr.AttestationObject, &p.AttestationObject)
if err != nil {
return nil, ErrParsingData.WithInfo(err.Error())
}
// Step 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
// structure to obtain the attestation statement format fmt, the authenticator data authData, and
// the attestation statement attStmt.
err = p.AttestationObject.AuthData.Unmarshal(p.AttestationObject.RawAuthData)
if err != nil {
return nil, fmt.Errorf("error decoding auth data: %v", err)
}
if !p.AttestationObject.AuthData.Flags.HasAttestedCredentialData() {
return nil, ErrAttestationFormat.WithInfo("Attestation missing attested credential data flag")
}
return &p, nil
}
// Verify - Perform Steps 9 through 14 of registration verification, delegating Steps
func (attestationObject *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, verificationRequired bool) error {
// Steps 9 through 12 are verified against the auth data.
// These steps are identical to 11 through 14 for assertion
// so we handle them with AuthData
// Begin Step 9. Verify that the rpIdHash in authData is
// the SHA-256 hash of the RP ID expected by the RP.
rpIDHash := sha256.Sum256([]byte(relyingPartyID))
// Handle Steps 9 through 12
authDataVerificationError := attestationObject.AuthData.Verify(rpIDHash[:], nil, verificationRequired)
if authDataVerificationError != nil {
return authDataVerificationError
}
// Step 13. Determine the attestation statement format by performing a
// USASCII case-sensitive match on fmt against the set of supported
// WebAuthn Attestation Statement Format Identifier values. The up-to-date
// list of registered WebAuthn Attestation Statement Format Identifier
// values is maintained in the IANA registry of the same name
// [WebAuthn-Registries] (https://www.w3.org/TR/webauthn/#biblio-webauthn-registries).
// Since there is not an active registry yet, we'll check it against our internal
// Supported types.
// But first let's make sure attestation is present. If it isn't, we don't need to handle
// any of the following steps
if attestationObject.Format == "none" {
if len(attestationObject.AttStatement) != 0 {
return ErrAttestationFormat.WithInfo("Attestation format none with attestation present")
}
return nil
}
formatHandler, valid := attestationRegistry[attestationObject.Format]
if !valid {
return ErrAttestationFormat.WithInfo(fmt.Sprintf("Attestation format %s is unsupported", attestationObject.Format))
}
// Step 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using
// the attestation statement format fmts verification procedure given attStmt, authData and the hash of the serialized
// client data computed in step 7.
attestationType, _, err := formatHandler(*attestationObject, clientDataHash)
if err != nil {
return err.(*Error).WithInfo(attestationType)
}
return nil
}
+227
View File
@@ -0,0 +1,227 @@
package protocol
import (
"bytes"
"crypto/x509"
"encoding/asn1"
"fmt"
"github.com/duo-labs/webauthn/protocol/webauthncose"
)
var androidAttestationKey = "android-key"
func init() {
RegisterAttestationFormat(androidAttestationKey, verifyAndroidKeyFormat)
}
// From §8.4. https://www.w3.org/TR/webauthn/#android-key-attestation
// The android-key attestation statement looks like:
// $$attStmtType //= (
// fmt: "android-key",
// attStmt: androidStmtFormat
// )
// androidStmtFormat = {
// alg: COSEAlgorithmIdentifier,
// sig: bytes,
// x5c: [ credCert: bytes, * (caCert: bytes) ]
// }
func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
// Given the verification procedure inputs attStmt, authenticatorData and clientDataHash, the verification procedure is as follows:
// §8.4.1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
// the contained fields.
// Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm
// used to generate the attestation signature.
alg, present := att.AttStatement["alg"].(int64)
if !present {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value")
}
// Get the sig value - A byte string containing the attestation signature.
sig, present := att.AttStatement["sig"].([]byte)
if !present {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value")
}
// If x5c is not present, return an error
x5c, x509present := att.AttStatement["x5c"].([]interface{})
if !x509present {
// Handle Basic Attestation steps for the x509 Certificate
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving x5c value")
}
// §8.4.2. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
// using the public key in the first certificate in x5c with the algorithm specified in alg.
attCertBytes, valid := x5c[0].([]byte)
if !valid {
return androidAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
signatureData := append(att.RawAuthData, clientDataHash...)
attCert, err := x509.ParseCertificate(attCertBytes)
if err != nil {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
}
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
err = attCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), signatureData, sig)
if err != nil {
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err))
}
// Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData.
pubKey, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
if err != nil {
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
}
e := pubKey.(webauthncose.EC2PublicKeyData)
valid, err = e.Verify(signatureData, sig)
if err != nil || valid != true {
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
}
// §8.4.3. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
// attCert.Extensions
var attExtBytes []byte
for _, ext := range attCert.Extensions {
if ext.Id.Equal([]int{1, 3, 6, 1, 4, 1, 11129, 2, 1, 17}) {
attExtBytes = ext.Value
}
}
if len(attExtBytes) == 0 {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.3.6.1.4.1.11129.2.1.17")
}
// As noted in §8.4.1 (https://w3c.github.io/webauthn/#key-attstn-cert-requirements) the Android Key Attestation attestation certificate's
// android key attestation certificate extension data is identified by the OID "1.3.6.1.4.1.11129.2.1.17".
decoded := keyDescription{}
_, err = asn1.Unmarshal([]byte(attExtBytes), &decoded)
if err != nil {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to parse Android key attestation certificate extensions")
}
// Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
if 0 != bytes.Compare(decoded.AttestationChallenge, clientDataHash) {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation challenge not equal to clientDataHash")
}
// The AuthorizationList.allApplications field is not present on either authorization list (softwareEnforced nor teeEnforced), since PublicKeyCredential MUST be scoped to the RP ID.
if nil != decoded.SoftwareEnforced.AllApplications || nil != decoded.TeeEnforced.AllApplications {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains all applications field")
}
// For the following, use only the teeEnforced authorization list if the RP wants to accept only keys from a trusted execution environment, otherwise use the union of teeEnforced and softwareEnforced.
// The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED. (which == 0)
if KM_ORIGIN_GENERATED != decoded.SoftwareEnforced.Origin || KM_ORIGIN_GENERATED != decoded.TeeEnforced.Origin {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with origin not equal KM_ORIGIN_GENERATED")
}
// The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN. (which == 2)
if !contains(decoded.SoftwareEnforced.Purpose, KM_PURPOSE_SIGN) && !contains(decoded.TeeEnforced.Purpose, KM_PURPOSE_SIGN) {
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with purpose not equal KM_PURPOSE_SIGN")
}
return androidAttestationKey, x5c, err
}
func contains(s []int, e int) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
type keyDescription struct {
AttestationVersion int
AttestationSecurityLevel asn1.Enumerated
KeymasterVersion int
KeymasterSecurityLevel asn1.Enumerated
AttestationChallenge []byte
UniqueID []byte
SoftwareEnforced authorizationList
TeeEnforced authorizationList
}
type authorizationList struct {
Purpose []int `asn1:"tag:1,explicit,set,optional"`
Algorithm int `asn1:"tag:2,explicit,optional"`
KeySize int `asn1:"tag:3,explicit,optional"`
Digest []int `asn1:"tag:5,explicit,set,optional"`
Padding []int `asn1:"tag:6,explicit,set,optional"`
EcCurve int `asn1:"tag:10,explicit,optional"`
RsaPublicExponent int `asn1:"tag:200,explicit,optional"`
RollbackResistance interface{} `asn1:"tag:303,explicit,optional"`
ActiveDateTime int `asn1:"tag:400,explicit,optional"`
OriginationExpireDateTime int `asn1:"tag:401,explicit,optional"`
UsageExpireDateTime int `asn1:"tag:402,explicit,optional"`
NoAuthRequired interface{} `asn1:"tag:503,explicit,optional"`
UserAuthType int `asn1:"tag:504,explicit,optional"`
AuthTimeout int `asn1:"tag:505,explicit,optional"`
AllowWhileOnBody interface{} `asn1:"tag:506,explicit,optional"`
TrustedUserPresenceRequired interface{} `asn1:"tag:507,explicit,optional"`
TrustedConfirmationRequired interface{} `asn1:"tag:508,explicit,optional"`
UnlockedDeviceRequired interface{} `asn1:"tag:509,explicit,optional"`
AllApplications interface{} `asn1:"tag:600,explicit,optional"`
ApplicationID interface{} `asn1:"tag:601,explicit,optional"`
CreationDateTime int `asn1:"tag:701,explicit,optional"`
Origin int `asn1:"tag:702,explicit,optional"`
RootOfTrust rootOfTrust `asn1:"tag:704,explicit,optional"`
OsVersion int `asn1:"tag:705,explicit,optional"`
OsPatchLevel int `asn1:"tag:706,explicit,optional"`
AttestationApplicationID []byte `asn1:"tag:709,explicit,optional"`
AttestationIDBrand []byte `asn1:"tag:710,explicit,optional"`
AttestationIDDevice []byte `asn1:"tag:711,explicit,optional"`
AttestationIDProduct []byte `asn1:"tag:712,explicit,optional"`
AttestationIDSerial []byte `asn1:"tag:713,explicit,optional"`
AttestationIDImei []byte `asn1:"tag:714,explicit,optional"`
AttestationIDMeid []byte `asn1:"tag:715,explicit,optional"`
AttestationIDManufacturer []byte `asn1:"tag:716,explicit,optional"`
AttestationIDModel []byte `asn1:"tag:717,explicit,optional"`
VendorPatchLevel int `asn1:"tag:718,explicit,optional"`
BootPatchLevel int `asn1:"tag:719,explicit,optional"`
}
type rootOfTrust struct {
verifiedBootKey []byte
deviceLocked bool
verifiedBootState verifiedBootState
verifiedBootHash []byte
}
type verifiedBootState int
const (
Verified verifiedBootState = iota
SelfSigned
Unverified
Failed
)
/**
* The origin of a key (or pair), i.e. where it was generated. Note that KM_TAG_ORIGIN can be found
* in either the hardware-enforced or software-enforced list for a key, indicating whether the key
* is hardware or software-based. Specifically, a key with KM_ORIGIN_GENERATED in the
* hardware-enforced list is guaranteed never to have existed outide the secure hardware.
*/
type KM_KEY_ORIGIN int
const (
KM_ORIGIN_GENERATED = iota /* Generated in keymaster. Should not exist outside the TEE. */
KM_ORIGIN_DERIVED /* Derived inside keymaster. Likely exists off-device. */
KM_ORIGIN_IMPORTED /* Imported into keymaster. Existed as cleartext in Android. */
KM_ORIGIN_UNKNOWN /* Keymaster did not record origin. This value can only be seen on
* keys in a keymaster0 implementation. The keymaster0 adapter uses
* this value to document the fact that it is unkown whether the key
* was generated inside or imported into keymaster. */
)
/**
* Possible purposes of a key (or pair).
*/
type KM_PURPOSE int
const (
KM_PURPOSE_ENCRYPT = iota /* Usable with RSA, EC and AES keys. */
KM_PURPOSE_DECRYPT /* Usable with RSA, EC and AES keys. */
KM_PURPOSE_SIGN /* Usable with RSA, EC and HMAC keys. */
KM_PURPOSE_VERIFY /* Usable with RSA, EC and HMAC keys. */
KM_PURPOSE_DERIVE_KEY /* Usable with EC keys. */
KM_PURPOSE_WRAP /* Usable with wrapped keys. */
)
+104
View File
@@ -0,0 +1,104 @@
package protocol
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"fmt"
"math/big"
"github.com/duo-labs/webauthn/protocol/webauthncose"
)
var appleAttestationKey = "apple"
func init() {
RegisterAttestationFormat(appleAttestationKey, verifyAppleKeyFormat)
}
// From §8.8. https://www.w3.org/TR/webauthn-2/#sctn-apple-anonymous-attestation
// The apple attestation statement looks like:
// $$attStmtType //= (
// fmt: "apple",
// attStmt: appleStmtFormat
// )
// appleStmtFormat = {
// x5c: [ credCert: bytes, * (caCert: bytes) ]
// }
func verifyAppleKeyFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
// Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined
// above and perform CBOR decoding on it to extract the contained fields.
// If x5c is not present, return an error
x5c, x509present := att.AttStatement["x5c"].([]interface{})
if !x509present {
// Handle Basic Attestation steps for the x509 Certificate
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving x5c value")
}
credCertBytes, valid := x5c[0].([]byte)
if !valid {
return appleAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
credCert, err := x509.ParseCertificate(credCertBytes)
if err != nil {
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
}
// Step 2. Concatenate authenticatorData and clientDataHash to form nonceToHash.
nonceToHash := append(att.RawAuthData, clientDataHash...)
// Step 3. Perform SHA-256 hash of nonceToHash to produce nonce.
nonce := sha256.Sum256(nonceToHash)
// Step 4. Verify that nonce equals the value of the extension with OID 1.2.840.113635.100.8.2 in credCert.
var attExtBytes []byte
for _, ext := range credCert.Extensions {
if ext.Id.Equal([]int{1, 2, 840, 113635, 100, 8, 2}) {
attExtBytes = ext.Value
}
}
if len(attExtBytes) == 0 {
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.2.840.113635.100.8.2")
}
decoded := AppleAnonymousAttestation{}
_, err = asn1.Unmarshal([]byte(attExtBytes), &decoded)
if err != nil {
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to parse apple attestation certificate extensions")
}
if !bytes.Equal(decoded.Nonce, nonce[:]) || err != nil {
return appleAttestationKey, nil, ErrInvalidAttestation.WithDetails("Attestation certificate does not contain expected nonce")
}
// Step 5. Verify that the credential public key equals the Subject Public Key of credCert.
// TODO: Probably move this part to webauthncose.go
pubKey, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
if err != nil {
return appleAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
}
credPK := pubKey.(webauthncose.EC2PublicKeyData)
subjectPK := credCert.PublicKey.(*ecdsa.PublicKey)
credPKInfo := &ecdsa.PublicKey{
Curve: elliptic.P256(),
X: big.NewInt(0).SetBytes(credPK.XCoord),
Y: big.NewInt(0).SetBytes(credPK.YCoord),
}
if !credPKInfo.Equal(subjectPK) {
return appleAttestationKey, nil, ErrInvalidAttestation.WithDetails("Certificate public key does not match public key in authData")
}
// Step 6. If successful, return implementation-specific values representing attestation type Anonymization CA and attestation trust path x5c.
return appleAttestationKey, x5c, nil
}
// Apple has not yet publish schema for the extension(as of JULY 2021.)
type AppleAnonymousAttestation struct {
Nonce []byte `asn1:"tag:1,explicit"`
}
+278
View File
@@ -0,0 +1,278 @@
package protocol
import (
"bytes"
"crypto/x509"
"encoding/asn1"
"fmt"
"strings"
"time"
"github.com/duo-labs/webauthn/metadata"
uuid "github.com/satori/go.uuid"
"github.com/duo-labs/webauthn/protocol/webauthncose"
)
var packedAttestationKey = "packed"
func init() {
RegisterAttestationFormat(packedAttestationKey, verifyPackedFormat)
}
// From §8.2. https://www.w3.org/TR/webauthn/#packed-attestation
// The packed attestation statement looks like:
// packedStmtFormat = {
// alg: COSEAlgorithmIdentifier,
// sig: bytes,
// x5c: [ attestnCert: bytes, * (caCert: bytes) ]
// } OR
// {
// alg: COSEAlgorithmIdentifier, (-260 for ED256 / -261 for ED512)
// sig: bytes,
// ecdaaKeyId: bytes
// } OR
// {
// alg: COSEAlgorithmIdentifier
// sig: bytes,
// }
func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
// Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined
// above and perform CBOR decoding on it to extract the contained fields.
// Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm
// used to generate the attestation signature.
alg, present := att.AttStatement["alg"].(int64)
if !present {
return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value")
}
// Get the sig value - A byte string containing the attestation signature.
sig, present := att.AttStatement["sig"].([]byte)
if !present {
return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value")
}
// Step 2. If x5c is present, this indicates that the attestation type is not ECDAA.
x5c, x509present := att.AttStatement["x5c"].([]interface{})
if x509present {
// Handle Basic Attestation steps for the x509 Certificate
return handleBasicAttestation(sig, clientDataHash, att.RawAuthData, att.AuthData.AttData.AAGUID, alg, x5c)
}
// Step 3. If ecdaaKeyId is present, then the attestation type is ECDAA.
// Also make sure the we did not have an x509 then
ecdaaKeyID, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte)
if ecdaaKeyPresent {
// Handle ECDAA Attestation steps for the x509 Certificate
return handleECDAAAttesation(sig, clientDataHash, ecdaaKeyID)
}
// Step 4. If neither x5c nor ecdaaKeyId is present, self attestation is in use.
return handleSelfAttestation(alg, att.AuthData.AttData.CredentialPublicKey, att.RawAuthData, clientDataHash, sig)
}
// Handle the attestation steps laid out in
func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte, alg int64, x5c []interface{}) (string, []interface{}, error) {
// Step 2.1. Verify that sig is a valid signature over the concatenation of authenticatorData
// and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.
attestationType := "Packed (Basic)"
for _, c := range x5c {
cb, cv := c.([]byte)
if !cv {
return attestationType, x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
ct, err := x509.ParseCertificate(cb)
if err != nil {
return attestationType, x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
}
if ct.NotBefore.After(time.Now()) || ct.NotAfter.Before(time.Now()) {
return attestationType, x5c, ErrAttestationFormat.WithDetails("Cert in chain not time valid")
}
}
attCertBytes, valid := x5c[0].([]byte)
if !valid {
return attestationType, x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
signatureData := append(authData, clientDataHash...)
attCert, err := x509.ParseCertificate(attCertBytes)
if err != nil {
return attestationType, x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
}
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
err = attCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), signatureData, signature)
if err != nil {
return attestationType, x5c, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err))
}
// Step 2.2 Verify that attestnCert meets the requirements in §8.2.1 Packed attestation statement certificate requirements.
// §8.2.1 can be found here https://www.w3.org/TR/webauthn/#packed-attestation-cert-requirements
// Step 2.2.1 (from §8.2.1) Version MUST be set to 3 (which is indicated by an ASN.1 INTEGER with value 2).
if attCert.Version != 3 {
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate is incorrect version")
}
// Step 2.2.2 (from §8.2.1) Subject field MUST be set to:
// Subject-C
// ISO 3166 code specifying the country where the Authenticator vendor is incorporated (PrintableString)
// TODO: Find a good, useable, country code library. For now, check stringy-ness
subjectString := strings.Join(attCert.Subject.Country, "")
if subjectString == "" {
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Country Code is invalid")
}
// Subject-O
// Legal name of the Authenticator vendor (UTF8String)
subjectString = strings.Join(attCert.Subject.Organization, "")
if subjectString == "" {
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Organization is invalid")
}
// Subject-OU
// Literal string “Authenticator Attestation” (UTF8String)
subjectString = strings.Join(attCert.Subject.OrganizationalUnit, " ")
if subjectString != "Authenticator Attestation" {
// TODO: Implement a return error when I'm more certain this is general practice
}
// Subject-CN
// A UTF8String of the vendors choosing
subjectString = attCert.Subject.CommonName
if subjectString == "" {
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Common Name not set")
}
// TODO: And then what
// Step 2.2.3 (from §8.2.1) If the related attestation root certificate is used for multiple authenticator models,
// the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the
// AAGUID as a 16-byte OCTET STRING. The extension MUST NOT be marked as critical.
idFido := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 45724, 1, 1, 4}
var foundAAGUID []byte
for _, extension := range attCert.Extensions {
if extension.Id.Equal(idFido) {
if extension.Critical {
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Attestation certificate FIDO extension marked as critical")
}
foundAAGUID = extension.Value
}
}
// We validate the AAGUID as mentioned above
// This is not well defined in§8.2.1 but mentioned in step 2.3: we validate the AAGUID if it is present within the certificate
// and make sure it matches the auth data AAGUID
// Note that an X.509 Extension encodes the DER-encoding of the value in an OCTET STRING. Thus, the
// AAGUID MUST be wrapped in two OCTET STRINGS to be valid.
if len(foundAAGUID) > 0 {
unMarshalledAAGUID := []byte{}
asn1.Unmarshal(foundAAGUID, &unMarshalledAAGUID)
if !bytes.Equal(aaguid, unMarshalledAAGUID) {
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Certificate AAGUID does not match Auth Data certificate")
}
}
uuid, err := uuid.FromBytes(aaguid)
if meta, ok := metadata.Metadata[uuid]; ok {
for _, s := range meta.StatusReports {
if metadata.IsUndesiredAuthenticatorStatus(metadata.AuthenticatorStatus(s.Status)) {
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Authenticator with undesirable status encountered")
}
}
if attCert.Subject.CommonName != attCert.Issuer.CommonName {
var hasBasicFull = false
for _, a := range meta.MetadataStatement.AttestationTypes {
if metadata.AuthenticatorAttestationType(a) == metadata.AuthenticatorAttestationType(metadata.BasicFull) {
hasBasicFull = true
}
}
if !hasBasicFull {
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Attestation with full attestation from authentictor that does not support full attestation")
}
}
} else {
if metadata.Conformance {
return attestationType, x5c, ErrInvalidAttestation.WithDetails("AAGUID not found in metadata during conformance testing")
}
}
// Step 2.2.4 The Basic Constraints extension MUST have the CA component set to false.
if attCert.IsCA {
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Attestation certificate's Basic Constraints marked as CA")
}
// Note for 2.2.5 An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL
// Distribution Point extension [RFC5280](https://www.w3.org/TR/webauthn/#biblio-rfc5280) are
// both OPTIONAL as the status of many attestation certificates is available through authenticator
// metadata services. See, for example, the FIDO Metadata Service
// [FIDOMetadataService] (https://www.w3.org/TR/webauthn/#biblio-fidometadataservice)
// Step 2.4 If successful, return attestation type Basic and attestation trust path x5c.
// We don't handle trust paths yet but we're done
return attestationType, x5c, nil
}
func handleECDAAAttesation(signature, clientDataHash, ecdaaKeyID []byte) (string, []interface{}, error) {
return "Packed (ECDAA)", nil, ErrNotSpecImplemented
}
func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, signature []byte) (string, []interface{}, error) {
attestationType := "Packed (Self)"
// §4.1 Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData.
// §4.2 Verify that sig is a valid signature over the concatenation of authenticatorData and
// clientDataHash using the credential public key with alg.
verificationData := append(authData, clientDataHash...)
key, err := webauthncose.ParsePublicKey(pubKey)
if err != nil {
return attestationType, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the public key: %+v\n", err))
}
switch key.(type) {
case webauthncose.OKPPublicKeyData:
k := key.(webauthncose.OKPPublicKeyData)
err := verifyKeyAlgorithm(k.Algorithm, alg)
if err != nil {
return attestationType, nil, err
}
case webauthncose.EC2PublicKeyData:
k := key.(webauthncose.EC2PublicKeyData)
err := verifyKeyAlgorithm(k.Algorithm, alg)
if err != nil {
return attestationType, nil, err
}
case webauthncose.RSAPublicKeyData:
k := key.(webauthncose.RSAPublicKeyData)
err := verifyKeyAlgorithm(k.Algorithm, alg)
if err != nil {
return attestationType, nil, err
}
default:
return attestationType, nil, ErrInvalidAttestation.WithDetails("Error verifying the public key data")
}
valid, err := webauthncose.VerifySignature(key, verificationData, signature)
if !valid && err == nil {
return attestationType, nil, ErrInvalidAttestation.WithDetails("Unabled to verify signature")
}
return attestationType, nil, err
}
func verifyKeyAlgorithm(keyAlgorithm, attestedAlgorithm int64) error {
if keyAlgorithm != attestedAlgorithm {
return ErrInvalidAttestation.WithDetails("Public key algorithm does not equal att statement algorithm")
}
return nil
}
+143
View File
@@ -0,0 +1,143 @@
package protocol
import (
"bytes"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"fmt"
"time"
"github.com/duo-labs/webauthn/metadata"
jwt "github.com/golang-jwt/jwt/v4"
"github.com/mitchellh/mapstructure"
)
var safetyNetAttestationKey = "android-safetynet"
func init() {
RegisterAttestationFormat(safetyNetAttestationKey, verifySafetyNetFormat)
}
type SafetyNetResponse struct {
Nonce string `json:"nonce"`
TimestampMs int64 `json:"timestampMs"`
ApkPackageName string `json:"apkPackageName"`
ApkDigestSha256 string `json:"apkDigestSha256"`
CtsProfileMatch bool `json:"ctsProfileMatch"`
ApkCertificateDigestSha256 []interface{} `json:"apkCertificateDigestSha256"`
BasicIntegrity bool `json:"basicIntegrity"`
}
// Thanks to @koesie10 and @herrjemand for outlining how to support this type really well
// §8.5. Android SafetyNet Attestation Statement Format https://w3c.github.io/webauthn/#android-safetynet-attestation
// When the authenticator in question is a platform-provided Authenticator on certain Android platforms, the attestation
// statement is based on the SafetyNet API. In this case the authenticator data is completely controlled by the caller of
// the SafetyNet API (typically an application running on the Android platform) and the attestation statement only provides
// some statements about the health of the platform and the identity of the calling application. This attestation does not
// provide information regarding provenance of the authenticator and its associated data. Therefore platform-provided
// authenticators SHOULD make use of the Android Key Attestation when available, even if the SafetyNet API is also present.
func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
// The syntax of an Android Attestation statement is defined as follows:
// $$attStmtType //= (
// fmt: "android-safetynet",
// attStmt: safetynetStmtFormat
// )
// safetynetStmtFormat = {
// ver: text,
// response: bytes
// }
// §8.5.1 Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
// the contained fields.
// We have done this
// §8.5.2 Verify that response is a valid SafetyNet response of version ver.
version, present := att.AttStatement["ver"].(string)
if !present {
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to find the version of SafetyNet")
}
if version == "" {
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Not a proper version for SafetyNet")
}
// TODO: provide user the ability to designate their supported versions
response, present := att.AttStatement["response"].([]byte)
if !present {
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to find the SafetyNet response")
}
token, err := jwt.Parse(string(response), func(token *jwt.Token) (interface{}, error) {
chain := token.Header["x5c"].([]interface{})
o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string))))
n, err := base64.StdEncoding.Decode(o, []byte(chain[0].(string)))
cert, err := x509.ParseCertificate(o[:n])
return cert.PublicKey, err
})
if err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
}
// marshall the JWT payload into the safetynet response json
var safetyNetResponse SafetyNetResponse
err = mapstructure.Decode(token.Claims, &safetyNetResponse)
if err != nil {
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the SafetyNet response: %+v", err))
}
// §8.5.3 Verify that the nonce in the response is identical to the Base64 encoding of the SHA-256 hash of the concatenation
// of authenticatorData and clientDataHash.
nonceBuffer := sha256.Sum256(append(att.RawAuthData, clientDataHash...))
nonceBytes, err := base64.StdEncoding.DecodeString(safetyNetResponse.Nonce)
if !bytes.Equal(nonceBuffer[:], nonceBytes) || err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails("Invalid nonce for in SafetyNet response")
}
// §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate)
certChain := token.Header["x5c"].([]interface{})
l := make([]byte, base64.StdEncoding.DecodedLen(len(certChain[0].(string))))
n, err := base64.StdEncoding.Decode(l, []byte(certChain[0].(string)))
if err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
}
attestationCert, err := x509.ParseCertificate(l[:n])
if err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
}
// §8.5.5 Verify that attestationCert is issued to the hostname "attest.android.com"
err = attestationCert.VerifyHostname("attest.android.com")
if err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
}
// §8.5.6 Verify that the ctsProfileMatch attribute in the payload of response is true.
if !safetyNetResponse.CtsProfileMatch {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails("ctsProfileMatch attribute of the JWT payload is false")
}
// Verify sanity of timestamp in the payload
now := time.Now()
oneMinuteAgo := now.Add(-time.Minute)
t := time.Unix(safetyNetResponse.TimestampMs/1000, 0)
if t.After(now) {
// zero tolerance for post-dated timestamps
return "Basic attestation with SafetyNet", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time")
} else if t.Before(oneMinuteAgo) {
// allow old timestamp for testing purposes
// TODO: Make this user configurable
msg := "SafetyNet response with timestamp before one minute ago"
if metadata.Conformance {
return "Basic attestation with SafetyNet", nil, ErrInvalidAttestation.WithDetails(msg)
}
}
// §8.5.7 If successful, return implementation-specific values representing attestation type Basic and attestation
// trust path attestationCert.
return "Basic attestation with SafetyNet", nil, nil
}
+349
View File
@@ -0,0 +1,349 @@
package protocol
import (
"bytes"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
"math/big"
"strings"
"github.com/duo-labs/webauthn/protocol/webauthncose"
"github.com/duo-labs/webauthn/protocol/googletpm"
)
var tpmAttestationKey = "tpm"
func init() {
RegisterAttestationFormat(tpmAttestationKey, verifyTPMFormat)
googletpm.UseTPM20LengthPrefixSize()
}
func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
// Given the verification procedure inputs attStmt, authenticatorData
// and clientDataHash, the verification procedure is as follows
// Verify that attStmt is valid CBOR conforming to the syntax defined
// above and perform CBOR decoding on it to extract the contained fields
ver, present := att.AttStatement["ver"].(string)
if !present {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving ver value")
}
if ver != "2.0" {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("WebAuthn only supports TPM 2.0 currently")
}
alg, present := att.AttStatement["alg"].(int64)
if !present {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value")
}
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
x5c, x509present := att.AttStatement["x5c"].([]interface{})
if !x509present {
// Handle Basic Attestation steps for the x509 Certificate
return tpmAttestationKey, nil, ErrNotImplemented
}
_, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte)
if ecdaaKeyPresent {
return tpmAttestationKey, nil, ErrNotImplemented
}
sigBytes, present := att.AttStatement["sig"].([]byte)
if !present {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value")
}
certInfoBytes, present := att.AttStatement["certInfo"].([]byte)
if !present {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving certInfo value")
}
pubAreaBytes, present := att.AttStatement["pubArea"].([]byte)
if !present {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving pubArea value")
}
// Verify that the public key specified by the parameters and unique fields of pubArea
// is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData.
pubArea, err := googletpm.DecodePublic(pubAreaBytes)
if err != nil {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to decode TPMT_PUBLIC in attestation statement")
}
key, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
switch key.(type) {
case webauthncose.EC2PublicKeyData:
e := key.(webauthncose.EC2PublicKeyData)
if pubArea.ECCParameters.CurveID != googletpm.EllipticCurve(e.Curve) ||
0 != pubArea.ECCParameters.Point.X.Cmp(new(big.Int).SetBytes(e.XCoord)) ||
0 != pubArea.ECCParameters.Point.Y.Cmp(new(big.Int).SetBytes(e.YCoord)) {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Mismatch between ECCParameters in pubArea and credentialPublicKey")
}
case webauthncose.RSAPublicKeyData:
r := key.(webauthncose.RSAPublicKeyData)
mod := new(big.Int).SetBytes(r.Modulus)
exp := uint32(r.Exponent[0]) + uint32(r.Exponent[1])<<8 + uint32(r.Exponent[2])<<16
if 0 != pubArea.RSAParameters.Modulus.Cmp(mod) ||
pubArea.RSAParameters.Exponent != exp {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey")
}
default:
return "", nil, ErrUnsupportedKey
}
// Concatenate authenticatorData and clientDataHash to form attToBeSigned
attToBeSigned := append(att.RawAuthData, clientDataHash...)
// Validate that certInfo is valid:
certInfo, err := googletpm.DecodeAttestationData(certInfoBytes)
// 1/4 Verify that magic is set to TPM_GENERATED_VALUE.
if certInfo.Magic != 0xff544347 {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Magic is not set to TPM_GENERATED_VALUE")
}
// 2/4 Verify that type is set to TPM_ST_ATTEST_CERTIFY.
if certInfo.Type != googletpm.TagAttestCertify {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Type is not set to TPM_ST_ATTEST_CERTIFY")
}
// 3/4 Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".
f := webauthncose.HasherFromCOSEAlg(coseAlg)
h := f()
h.Write(attToBeSigned)
if 0 != bytes.Compare(certInfo.ExtraData, h.Sum(nil)) {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("ExtraData is not set to hash of attToBeSigned")
}
// 4/4 Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in
// [TPMv2-Part2] section 10.12.3, whose name field contains a valid Name for pubArea,
// as computed using the algorithm in the nameAlg field of pubArea
// using the procedure specified in [TPMv2-Part1] section 16.
f, err = certInfo.AttestedCertifyInfo.Name.Digest.Alg.HashConstructor()
h = f()
h.Write(pubAreaBytes)
if 0 != bytes.Compare(h.Sum(nil), certInfo.AttestedCertifyInfo.Name.Digest.Value) {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Hash value mismatch attested and pubArea")
}
// Note that the remaining fields in the "Standard Attestation Structure"
// [TPMv2-Part1] section 31.2, i.e., qualifiedSigner, clockInfo and firmwareVersion
// are ignored. These fields MAY be used as an input to risk engines.
// If x5c is present, this indicates that the attestation type is not ECDAA.
if x509present {
// In this case:
// Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the algorithm specified in alg.
aikCertBytes, valid := x5c[0].([]byte)
if !valid {
return tpmAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
aikCert, err := x509.ParseCertificate(aikCertBytes)
if err != nil {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1")
}
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
err = aikCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), certInfoBytes, sigBytes)
if err != nil {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err))
}
// Verify that aikCert meets the requirements in §8.3.1 TPM Attestation Statement Certificate Requirements
// 1/6 Version MUST be set to 3.
if aikCert.Version != 3 {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate version must be 3")
}
// 2/6 Subject field MUST be set to empty.
if aikCert.Subject.String() != "" {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate subject must be empty")
}
// 3/6 The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9{}
var manufacturer, model, version string
for _, ext := range aikCert.Extensions {
if ext.Id.Equal([]int{2, 5, 29, 17}) {
manufacturer, model, version, err = parseSANExtension(ext.Value)
}
}
if manufacturer == "" || model == "" || version == "" {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Invalid SAN data in AIK certificate")
}
if false == isValidTPMManufacturer(manufacturer) {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Invalid TPM manufacturer")
}
// 4/6 The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID.
var ekuValid = false
var eku []asn1.ObjectIdentifier
for _, ext := range aikCert.Extensions {
if ext.Id.Equal([]int{2, 5, 29, 37}) {
rest, err := asn1.Unmarshal(ext.Value, &eku)
if len(rest) != 0 || err != nil || !eku[0].Equal(tcgKpAIKCertificate) {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate EKU missing 2.23.133.8.3")
}
ekuValid = true
}
}
if false == ekuValid {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate missing EKU")
}
// 5/6 The Basic Constraints extension MUST have the CA component set to false.
type basicConstraints struct {
IsCA bool `asn1:"optional"`
MaxPathLen int `asn1:"optional,default:-1"`
}
var constraints basicConstraints
for _, ext := range aikCert.Extensions {
if ext.Id.Equal([]int{2, 5, 29, 19}) {
if rest, err := asn1.Unmarshal(ext.Value, &constraints); err != nil {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints malformed")
} else if len(rest) != 0 {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints contains extra data")
}
}
}
if constraints.IsCA != false {
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints missing or CA is true")
}
// 6/6 An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point
// extension [RFC5280] are both OPTIONAL as the status of many attestation certificates is available
// through metadata services. See, for example, the FIDO Metadata Service.
}
return tpmAttestationKey, x5c, err
}
func forEachSAN(extension []byte, callback func(tag int, data []byte) error) error {
// RFC 5280, 4.2.1.6
// SubjectAltName ::= GeneralNames
//
// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
//
// GeneralName ::= CHOICE {
// otherName [0] OtherName,
// rfc822Name [1] IA5String,
// dNSName [2] IA5String,
// x400Address [3] ORAddress,
// directoryName [4] Name,
// ediPartyName [5] EDIPartyName,
// uniformResourceIdentifier [6] IA5String,
// iPAddress [7] OCTET STRING,
// registeredID [8] OBJECT IDENTIFIER }
var seq asn1.RawValue
rest, err := asn1.Unmarshal(extension, &seq)
if err != nil {
return err
} else if len(rest) != 0 {
return errors.New("x509: trailing data after X.509 extension")
}
if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 {
return asn1.StructuralError{Msg: "bad SAN sequence"}
}
rest = seq.Bytes
for len(rest) > 0 {
var v asn1.RawValue
rest, err = asn1.Unmarshal(rest, &v)
if err != nil {
return err
}
if err := callback(v.Tag, v.Bytes); err != nil {
return err
}
}
return nil
}
const (
nameTypeDN = 4
)
var (
tcgKpAIKCertificate = asn1.ObjectIdentifier{2, 23, 133, 8, 3}
tcgAtTpmManufacturer = asn1.ObjectIdentifier{2, 23, 133, 2, 1}
tcgAtTpmModel = asn1.ObjectIdentifier{2, 23, 133, 2, 2}
tcgAtTpmVersion = asn1.ObjectIdentifier{2, 23, 133, 2, 3}
)
func parseSANExtension(value []byte) (manufacturer string, model string, version string, err error) {
err = forEachSAN(value, func(tag int, data []byte) error {
switch tag {
case nameTypeDN:
tpmDeviceAttributes := pkix.RDNSequence{}
_, err := asn1.Unmarshal(data, &tpmDeviceAttributes)
if err != nil {
return err
}
for _, rdn := range tpmDeviceAttributes {
if len(rdn) == 0 {
continue
}
for _, atv := range rdn {
value, ok := atv.Value.(string)
if !ok {
continue
}
if atv.Type.Equal(tcgAtTpmManufacturer) {
manufacturer = strings.TrimPrefix(value, "id:")
}
if atv.Type.Equal(tcgAtTpmModel) {
model = value
}
if atv.Type.Equal(tcgAtTpmVersion) {
version = strings.TrimPrefix(value, "id:")
}
}
}
}
return nil
})
return
}
var tpmManufacturers = []struct {
id string
name string
code string
}{
{"414D4400", "AMD", "AMD"},
{"41544D4C", "Atmel", "ATML"},
{"4252434D", "Broadcom", "BRCM"},
{"49424d00", "IBM", "IBM"},
{"49465800", "Infineon", "IFX"},
{"494E5443", "Intel", "INTC"},
{"4C454E00", "Lenovo", "LEN"},
{"4E534D20", "National Semiconductor", "NSM"},
{"4E545A00", "Nationz", "NTZ"},
{"4E544300", "Nuvoton Technology", "NTC"},
{"51434F4D", "Qualcomm", "QCOM"},
{"534D5343", "SMSC", "SMSC"},
{"53544D20", "ST Microelectronics", "STM"},
{"534D534E", "Samsung", "SMSN"},
{"534E5300", "Sinosun", "SNS"},
{"54584E00", "Texas Instruments", "TXN"},
{"57454300", "Winbond", "WEC"},
{"524F4343", "Fuzhouk Rockchip", "ROCC"},
{"FFFFF1D0", "FIDO Alliance Conformance Testing", "FIDO"},
}
func isValidTPMManufacturer(id string) bool {
for _, m := range tpmManufacturers {
if m.id == id {
return true
}
}
return false
}
+135
View File
@@ -0,0 +1,135 @@
package protocol
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/x509"
"github.com/duo-labs/webauthn/protocol/webauthncose"
"github.com/fxamacker/cbor/v2"
)
var u2fAttestationKey = "fido-u2f"
func init() {
RegisterAttestationFormat(u2fAttestationKey, verifyU2FFormat)
}
// verifyU2FFormat - Follows verification steps set out by https://www.w3.org/TR/webauthn/#fido-u2f-attestation
func verifyU2FFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
if !bytes.Equal(att.AuthData.AttData.AAGUID, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) {
return u2fAttestationKey, nil, ErrUnsupportedAlgorithm.WithDetails("U2F attestation format AAGUID not set to 0x00")
}
// Signing procedure step - If the credential public key of the given credential is not of
// algorithm -7 ("ES256"), stop and return an error.
key := webauthncose.EC2PublicKeyData{}
cbor.Unmarshal(att.AuthData.AttData.CredentialPublicKey, &key)
if webauthncose.COSEAlgorithmIdentifier(key.PublicKeyData.Algorithm) != webauthncose.AlgES256 {
return u2fAttestationKey, nil, ErrUnsupportedAlgorithm.WithDetails("Non-ES256 Public Key algorithm used")
}
// U2F Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined above
// and perform CBOR decoding on it to extract the contained fields.
// The Format/syntax is
// u2fStmtFormat = {
// x5c: [ attestnCert: bytes ],
// sig: bytes
// }
// Check for "x5c" which is a single element array containing the attestation certificate in X.509 format.
x5c, present := att.AttStatement["x5c"].([]interface{})
if !present {
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Missing properly formatted x5c data")
}
// Check for "sig" which is The attestation signature. The signature was calculated over the (raw) U2F
// registration response message https://www.w3.org/TR/webauthn/#biblio-fido-u2f-message-formats]
// received by the client from the authenticator.
signature, present := att.AttStatement["sig"].([]byte)
if !present {
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Missing sig data")
}
// U2F Step 2. (1) Check that x5c has exactly one element and let attCert be that element. (2) Let certificate public
// key be the public key conveyed by attCert. (3) If certificate public key is not an Elliptic Curve (EC) public
// key over the P-256 curve, terminate this algorithm and return an appropriate error.
// Step 2.1
if len(x5c) > 1 {
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Received more than one element in x5c values")
}
// Note: Packed Attestation, FIDO U2F Attestation, and Assertion Signatures support ASN.1,but it is recommended
// that any new attestation formats defined not use ASN.1 encodings, but instead represent signatures as equivalent
// fixed-length byte arrays without internal structure, using the same representations as used by COSE signatures
// as defined in RFC8152 (https://www.w3.org/TR/webauthn/#biblio-rfc8152)
// and RFC8230 (https://www.w3.org/TR/webauthn/#biblio-rfc8230).
// Step 2.2
asn1Bytes, decoded := x5c[0].([]byte)
if !decoded {
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Error decoding ASN.1 data from x5c")
}
attCert, err := x509.ParseCertificate(asn1Bytes)
if err != nil {
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1 data into certificate")
}
// Step 2.3
if attCert.PublicKeyAlgorithm != x509.ECDSA && attCert.PublicKey.(*ecdsa.PublicKey).Curve != elliptic.P256() {
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate is in invalid format")
}
// Step 3. Extract the claimed rpIdHash from authenticatorData, and the claimed credentialId and credentialPublicKey
// from authenticatorData.attestedCredentialData.
rpIdHash := att.AuthData.RPIDHash
credentialID := att.AuthData.AttData.CredentialID
// credentialPublicKey handled earlier
// Step 4. Convert the COSE_KEY formatted credentialPublicKey (see Section 7 of RFC8152 [https://www.w3.org/TR/webauthn/#biblio-rfc8152])
// to Raw ANSI X9.62 public key format (see ALG_KEY_ECC_X962_RAW in Section 3.6.2 Public Key
// Representation Formats of FIDO-Registry [https://www.w3.org/TR/webauthn/#biblio-fido-registry]).
// Let x be the value corresponding to the "-2" key (representing x coordinate) in credentialPublicKey, and confirm
// its size to be of 32 bytes. If size differs or "-2" key is not found, terminate this algorithm and
// return an appropriate error.
// Let y be the value corresponding to the "-3" key (representing y coordinate) in credentialPublicKey, and confirm
// its size to be of 32 bytes. If size differs or "-3" key is not found, terminate this algorithm and
// return an appropriate error.
if len(key.XCoord) > 32 || len(key.YCoord) > 32 {
return u2fAttestationKey, nil, ErrAttestation.WithDetails("X or Y Coordinate for key is invalid length")
}
// Let publicKeyU2F be the concatenation 0x04 || x || y.
publicKeyU2F := bytes.NewBuffer([]byte{0x04})
publicKeyU2F.Write(key.XCoord)
publicKeyU2F.Write(key.YCoord)
// Step 5. Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F)
// (see §4.3 of FIDO-U2F-Message-Formats [https://www.w3.org/TR/webauthn/#biblio-fido-u2f-message-formats]).
verificationData := bytes.NewBuffer([]byte{0x00})
verificationData.Write(rpIdHash)
verificationData.Write(clientDataHash)
verificationData.Write(credentialID)
verificationData.Write(publicKeyU2F.Bytes())
// Step 6. Verify the sig using verificationData and certificate public key per SEC1[https://www.w3.org/TR/webauthn/#biblio-sec1].
sigErr := attCert.CheckSignature(x509.ECDSAWithSHA256, verificationData.Bytes(), signature)
if sigErr != nil {
return u2fAttestationKey, nil, sigErr
}
// Step 7. If successful, return attestation type Basic with the attestation trust path set to x5c.
return "Fido U2F Basic", x5c, sigErr
}
+256
View File
@@ -0,0 +1,256 @@
package protocol
import (
"bytes"
"encoding/binary"
"fmt"
"github.com/fxamacker/cbor/v2"
)
var minAuthDataLength = 37
// Authenticators respond to Relying Party requests by returning an object derived from the
// AuthenticatorResponse interface. See §5.2. Authenticator Responses
// https://www.w3.org/TR/webauthn/#iface-authenticatorresponse
type AuthenticatorResponse struct {
// From the spec https://www.w3.org/TR/webauthn/#dom-authenticatorresponse-clientdatajson
// This attribute contains a JSON serialization of the client data passed to the authenticator
// by the client in its call to either create() or get().
ClientDataJSON URLEncodedBase64 `json:"clientDataJSON"`
}
// AuthenticatorData From §6.1 of the spec.
// The authenticator data structure encodes contextual bindings made by the authenticator. These bindings
// are controlled by the authenticator itself, and derive their trust from the WebAuthn Relying Party's
// assessment of the security properties of the authenticator. In one extreme case, the authenticator
// may be embedded in the client, and its bindings may be no more trustworthy than the client data.
// At the other extreme, the authenticator may be a discrete entity with high-security hardware and
// software, connected to the client over a secure channel. In both cases, the Relying Party receives
// the authenticator data in the same format, and uses its knowledge of the authenticator to make
// trust decisions.
//
// The authenticator data, at least during attestation, contains the Public Key that the RP stores
// and will associate with the user attempting to register.
type AuthenticatorData struct {
RPIDHash []byte `json:"rpid"`
Flags AuthenticatorFlags `json:"flags"`
Counter uint32 `json:"sign_count"`
AttData AttestedCredentialData `json:"att_data"`
ExtData []byte `json:"ext_data"`
}
type AttestedCredentialData struct {
AAGUID []byte `json:"aaguid"`
CredentialID []byte `json:"credential_id"`
// The raw credential public key bytes received from the attestation data
CredentialPublicKey []byte `json:"public_key"`
}
// AuthenticatorAttachment https://www.w3.org/TR/webauthn/#platform-attachment
type AuthenticatorAttachment string
const (
// Platform - A platform authenticator is attached using a client device-specific transport, called
// platform attachment, and is usually not removable from the client device. A public key credential
// bound to a platform authenticator is called a platform credential.
Platform AuthenticatorAttachment = "platform"
// CrossPlatform A roaming authenticator is attached using cross-platform transports, called
// cross-platform attachment. Authenticators of this class are removable from, and can "roam"
// among, client devices. A public key credential bound to a roaming authenticator is called a
// roaming credential.
CrossPlatform AuthenticatorAttachment = "cross-platform"
)
// Authenticators may implement various transports for communicating with clients. This enumeration defines
// hints as to how clients might communicate with a particular authenticator in order to obtain an assertion
// for a specific credential. Note that these hints represent the WebAuthn Relying Party's best belief as to
// how an authenticator may be reached. A Relying Party may obtain a list of transports hints from some
// attestation statement formats or via some out-of-band mechanism; it is outside the scope of this
// specification to define that mechanism.
// See §5.10.4. Authenticator Transport https://www.w3.org/TR/webauthn/#transport
type AuthenticatorTransport string
const (
// USB The authenticator should transport information over USB
USB AuthenticatorTransport = "usb"
// NFC The authenticator should transport information over Near Field Communication Protocol
NFC AuthenticatorTransport = "nfc"
// BLE The authenticator should transport information over Bluetooth
BLE AuthenticatorTransport = "ble"
// Internal the client should use an internal source like a TPM or SE
Internal AuthenticatorTransport = "internal"
)
// A WebAuthn Relying Party may require user verification for some of its operations but not for others,
// and may use this type to express its needs.
// See §5.10.6. User Verification Requirement Enumeration https://www.w3.org/TR/webauthn/#userVerificationRequirement
type UserVerificationRequirement string
const (
// VerificationRequired User verification is required to create/release a credential
VerificationRequired UserVerificationRequirement = "required"
// VerificationPreferred User verification is preferred to create/release a credential
VerificationPreferred UserVerificationRequirement = "preferred" // This is the default
// VerificationDiscouraged The authenticator should not verify the user for the credential
VerificationDiscouraged UserVerificationRequirement = "discouraged"
)
// AuthenticatorFlags A byte of information returned during during ceremonies in the
// authenticatorData that contains bits that give us information about the
// whether the user was present and/or verified during authentication, and whether
// there is attestation or extension data present. Bit 0 is the least significant bit.
type AuthenticatorFlags byte
// The bits that do not have flags are reserved for future use.
const (
// FlagUserPresent Bit 00000001 in the byte sequence. Tells us if user is present
FlagUserPresent AuthenticatorFlags = 1 << iota // Referred to as UP
_ // Reserved
// FlagUserVerified Bit 00000100 in the byte sequence. Tells us if user is verified
// by the authenticator using a biometric or PIN
FlagUserVerified // Referred to as UV
_ // Reserved
_ // Reserved
_ // Reserved
// FlagAttestedCredentialData Bit 01000000 in the byte sequence. Indicates whether
// the authenticator added attested credential data.
FlagAttestedCredentialData // Referred to as AT
// FlagHasExtension Bit 10000000 in the byte sequence. Indicates if the authenticator data has extensions.
FlagHasExtensions // Referred to as ED
)
// UserPresent returns if the UP flag was set
func (flag AuthenticatorFlags) UserPresent() bool {
return (flag & FlagUserPresent) == FlagUserPresent
}
// UserVerified returns if the UV flag was set
func (flag AuthenticatorFlags) UserVerified() bool {
return (flag & FlagUserVerified) == FlagUserVerified
}
// HasAttestedCredentialData returns if the AT flag was set
func (flag AuthenticatorFlags) HasAttestedCredentialData() bool {
return (flag & FlagAttestedCredentialData) == FlagAttestedCredentialData
}
// HasExtensions returns if the ED flag was set
func (flag AuthenticatorFlags) HasExtensions() bool {
return (flag & FlagHasExtensions) == FlagHasExtensions
}
// Unmarshal will take the raw Authenticator Data and marshalls it into AuthenticatorData for further validation.
// The authenticator data has a compact but extensible encoding. This is desired since authenticators can be
// devices with limited capabilities and low power requirements, with much simpler software stacks than the client platform.
// The authenticator data structure is a byte array of 37 bytes or more, and is laid out in this table:
// https://www.w3.org/TR/webauthn/#table-authData
func (a *AuthenticatorData) Unmarshal(rawAuthData []byte) error {
if minAuthDataLength > len(rawAuthData) {
err := ErrBadRequest.WithDetails("Authenticator data length too short")
info := fmt.Sprintf("Expected data greater than %d bytes. Got %d bytes\n", minAuthDataLength, len(rawAuthData))
return err.WithInfo(info)
}
a.RPIDHash = rawAuthData[:32]
a.Flags = AuthenticatorFlags(rawAuthData[32])
a.Counter = binary.BigEndian.Uint32(rawAuthData[33:37])
remaining := len(rawAuthData) - minAuthDataLength
if a.Flags.HasAttestedCredentialData() {
if len(rawAuthData) > minAuthDataLength {
a.unmarshalAttestedData(rawAuthData)
attDataLen := len(a.AttData.AAGUID) + 2 + len(a.AttData.CredentialID) + len(a.AttData.CredentialPublicKey)
remaining = remaining - attDataLen
} else {
return ErrBadRequest.WithDetails("Attested credential flag set but data is missing")
}
} else {
if !a.Flags.HasExtensions() && len(rawAuthData) != 37 {
return ErrBadRequest.WithDetails("Attested credential flag not set")
}
}
if a.Flags.HasExtensions() {
if remaining != 0 {
a.ExtData = rawAuthData[len(rawAuthData)-remaining:]
remaining -= len(a.ExtData)
} else {
return ErrBadRequest.WithDetails("Extensions flag set but extensions data is missing")
}
}
if remaining != 0 {
return ErrBadRequest.WithDetails("Leftover bytes decoding AuthenticatorData")
}
return nil
}
// If Attestation Data is present, unmarshall that into the appropriate public key structure
func (a *AuthenticatorData) unmarshalAttestedData(rawAuthData []byte) {
a.AttData.AAGUID = rawAuthData[37:53]
idLength := binary.BigEndian.Uint16(rawAuthData[53:55])
a.AttData.CredentialID = rawAuthData[55 : 55+idLength]
a.AttData.CredentialPublicKey = unmarshalCredentialPublicKey(rawAuthData[55+idLength:])
}
// Unmarshall the credential's Public Key into CBOR encoding
func unmarshalCredentialPublicKey(keyBytes []byte) []byte {
var m interface{}
cbor.Unmarshal(keyBytes, &m)
rawBytes, _ := cbor.Marshal(m)
return rawBytes
}
// ResidentKeyRequired - Require that the key be private key resident to the client device
func ResidentKeyRequired() *bool {
required := true
return &required
}
// ResidentKeyUnrequired - Do not require that the private key be resident to the client device.
func ResidentKeyUnrequired() *bool {
required := false
return &required
}
// Verify on AuthenticatorData handles Steps 9 through 12 for Registration
// and Steps 11 through 14 for Assertion.
func (a *AuthenticatorData) Verify(rpIdHash, appIDHash []byte, userVerificationRequired bool) error {
// Registration Step 9 & Assertion Step 11
// Verify that the RP ID hash in authData is indeed the SHA-256
// hash of the RP ID expected by the RP.
if !bytes.Equal(a.RPIDHash[:], rpIdHash) && !bytes.Equal(a.RPIDHash[:], appIDHash) {
return ErrVerification.WithInfo(fmt.Sprintf("RP Hash mismatch. Expected %s and Received %s\n", a.RPIDHash, rpIdHash))
}
// Registration Step 10 & Assertion Step 12
// Verify that the User Present bit of the flags in authData is set.
if !a.Flags.UserPresent() {
return ErrVerification.WithInfo(fmt.Sprintln("User presence flag not set by authenticator"))
}
// Registration Step 11 & Assertion Step 13
// If user verification is required for this assertion, verify that
// the User Verified bit of the flags in authData is set.
if userVerificationRequired && !a.Flags.UserVerified() {
return ErrVerification.WithInfo(fmt.Sprintln("User verification required but flag not set by authenticator"))
}
// Registration Step 12 & Assertion Step 14
// Verify that the values of the client extension outputs in clientExtensionResults
// and the authenticator extension outputs in the extensions in authData are as
// expected, considering the client extension input values that were given as the
// extensions option in the create() call. In particular, any extension identifier
// values in the clientExtensionResults and the extensions in authData MUST be also be
// present as extension identifier values in the extensions member of options, i.e., no
// extensions are present that were not requested. In the general case, the meaning
// of "are as expected" is specific to the Relying Party and which extensions are in use.
// This is not yet fully implemented by the spec or by browsers
return nil
}
+41
View File
@@ -0,0 +1,41 @@
package protocol
import (
"bytes"
"encoding/base64"
"reflect"
)
// URLEncodedBase64 represents a byte slice holding URL-encoded base64 data.
// When fields of this type are unmarshaled from JSON, the data is base64
// decoded into a byte slice.
type URLEncodedBase64 []byte
// UnmarshalJSON base64 decodes a URL-encoded value, storing the result in the
// provided byte slice.
func (dest *URLEncodedBase64) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("null")) {
return nil
}
// Trim the leading spaces
data = bytes.Trim(data, "\"")
out := make([]byte, base64.RawURLEncoding.DecodedLen(len(data)))
n, err := base64.RawURLEncoding.Decode(out, data)
if err != nil {
return err
}
v := reflect.ValueOf(dest).Elem()
v.SetBytes(out[:n])
return nil
}
// MarshalJSON base64 encodes a non URL-encoded value, storing the result in the
// provided byte slice.
func (data URLEncodedBase64) MarshalJSON() ([]byte, error) {
if data == nil {
return []byte("null"), nil
}
return []byte(`"` + base64.RawURLEncoding.EncodeToString(data) + `"`), nil
}
+27
View File
@@ -0,0 +1,27 @@
package protocol
import (
"crypto/rand"
"encoding/base64"
)
// ChallengeLength - Length of bytes to generate for a challenge
const ChallengeLength = 32
// Challenge that should be signed and returned by the authenticator
type Challenge URLEncodedBase64
// Create a new challenge to be sent to the authenticator. The spec recommends using
// at least 16 bytes with 100 bits of entropy. We use 32 bytes.
func CreateChallenge() (Challenge, error) {
challenge := make([]byte, ChallengeLength)
_, err := rand.Read(challenge)
if err != nil {
return nil, err
}
return challenge, nil
}
func (c Challenge) String() string {
return base64.RawURLEncoding.EncodeToString(c)
}
+112
View File
@@ -0,0 +1,112 @@
package protocol
import (
"fmt"
"net/url"
"strings"
)
// CollectedClientData represents the contextual bindings of both the WebAuthn Relying Party
// and the client. It is a key-value mapping whose keys are strings. Values can be any type
// that has a valid encoding in JSON. Its structure is defined by the following Web IDL.
// https://www.w3.org/TR/webauthn/#sec-client-data
type CollectedClientData struct {
// Type the string "webauthn.create" when creating new credentials,
// and "webauthn.get" when getting an assertion from an existing credential. The
// purpose of this member is to prevent certain types of signature confusion attacks
//(where an attacker substitutes one legitimate signature for another).
Type CeremonyType `json:"type"`
Challenge string `json:"challenge"`
Origin string `json:"origin"`
TokenBinding *TokenBinding `json:"tokenBinding,omitempty"`
// Chromium (Chrome) returns a hint sometimes about how to handle clientDataJSON in a safe manner
Hint string `json:"new_keys_may_be_added_here,omitempty"`
}
type CeremonyType string
const (
CreateCeremony CeremonyType = "webauthn.create"
AssertCeremony CeremonyType = "webauthn.get"
)
type TokenBinding struct {
Status TokenBindingStatus `json:"status"`
ID string `json:"id,omitempty"`
}
type TokenBindingStatus string
const (
// Indicates token binding was used when communicating with the
// Relying Party. In this case, the id member MUST be present.
Present TokenBindingStatus = "present"
// Indicates token binding was used when communicating with the
// negotiated when communicating with the Relying Party.
Supported TokenBindingStatus = "supported"
// Indicates token binding not supported
// when communicating with the Relying Party.
NotSupported TokenBindingStatus = "not-supported"
)
// Returns the origin per the HTML spec: (scheme)://(host)[:(port)]
func FullyQualifiedOrigin(u *url.URL) string {
return fmt.Sprintf("%s://%s", u.Scheme, u.Host)
}
// Handles steps 3 through 6 of verfying the registering client data of a
// new credential and steps 7 through 10 of verifying an authentication assertion
// See https://www.w3.org/TR/webauthn/#registering-a-new-credential
// and https://www.w3.org/TR/webauthn/#verifying-assertion
func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyType, relyingPartyOrigin string) error {
// Registration Step 3. Verify that the value of C.type is webauthn.create.
// Assertion Step 7. Verify that the value of C.type is the string webauthn.get.
if c.Type != ceremony {
err := ErrVerification.WithDetails("Error validating ceremony type")
err.WithInfo(fmt.Sprintf("Expected Value: %s\n Received: %s\n", ceremony, c.Type))
return err
}
// Registration Step 4. Verify that the value of C.challenge matches the challenge
// that was sent to the authenticator in the create() call.
// Assertion Step 8. Verify that the value of C.challenge matches the challenge
// that was sent to the authenticator in the PublicKeyCredentialRequestOptions
// passed to the get() call.
challenge := c.Challenge
if 0 != strings.Compare(storedChallenge, challenge) {
err := ErrVerification.WithDetails("Error validating challenge")
return err.WithInfo(fmt.Sprintf("Expected b Value: %#v\nReceived b: %#v\n", storedChallenge, challenge))
}
// Registration Step 5 & Assertion Step 9. Verify that the value of C.origin matches
// the Relying Party's origin.
clientDataOrigin, err := url.Parse(c.Origin)
if err != nil {
return ErrParsingData.WithDetails("Error decoding clientData origin as URL")
}
if !strings.EqualFold(FullyQualifiedOrigin(clientDataOrigin), relyingPartyOrigin) {
err := ErrVerification.WithDetails("Error validating origin")
return err.WithInfo(fmt.Sprintf("Expected Value: %s\n Received: %s\n", relyingPartyOrigin, FullyQualifiedOrigin(clientDataOrigin)))
}
// Registration Step 6 and Assertion Step 10. Verify that the value of C.tokenBinding.status
// matches the state of Token Binding for the TLS connection over which the assertion was
// obtained. If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id
// matches the base64url encoding of the Token Binding ID for the connection.
if c.TokenBinding != nil {
if c.TokenBinding.Status == "" {
return ErrParsingData.WithDetails("Error decoding clientData, token binding present without status")
}
if c.TokenBinding.Status != Present && c.TokenBinding.Status != Supported && c.TokenBinding.Status != NotSupported {
return ErrParsingData.WithDetails("Error decoding clientData, token binding present with invalid status").WithInfo(fmt.Sprintf("Got: %s\n", c.TokenBinding.Status))
}
}
// Not yet fully implemented by the spec, browsers, and me.
return nil
}
+216
View File
@@ -0,0 +1,216 @@
package protocol
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"io"
"net/http"
)
// The basic credential type that is inherited by WebAuthn's
// PublicKeyCredential type
// https://w3c.github.io/webappsec-credential-management/#credential
type Credential struct {
// ID is The credentials identifier. The requirements for the
// identifier are distinct for each type of credential. It might
// represent a username for username/password tuples, for example.
ID string `json:"id"`
// Type is the value of the objects interface object's [[type]] slot,
// which specifies the credential type represented by this object.
// This should be type "public-key" for Webauthn credentials.
Type string `json:"type"`
}
// The PublicKeyCredential interface inherits from Credential, and contains
// the attributes that are returned to the caller when a new credential
// is created, or a new assertion is requested.
type ParsedCredential struct {
ID string `cbor:"id"`
Type string `cbor:"type"`
}
type PublicKeyCredential struct {
Credential
RawID URLEncodedBase64 `json:"rawId"`
ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
}
type ParsedPublicKeyCredential struct {
ParsedCredential
RawID []byte `json:"rawId"`
ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
}
type CredentialCreationResponse struct {
PublicKeyCredential
AttestationResponse AuthenticatorAttestationResponse `json:"response"`
}
type ParsedCredentialCreationData struct {
ParsedPublicKeyCredential
Response ParsedAttestationResponse
Raw CredentialCreationResponse
}
func ParseCredentialCreationResponse(response *http.Request) (*ParsedCredentialCreationData, error) {
if response == nil || response.Body == nil {
return nil, ErrBadRequest.WithDetails("No response given")
}
return ParseCredentialCreationResponseBody(response.Body)
}
func ParseCredentialCreationResponseBody(body io.Reader) (*ParsedCredentialCreationData, error) {
var ccr CredentialCreationResponse
err := json.NewDecoder(body).Decode(&ccr)
if err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo(err.Error())
}
if ccr.ID == "" {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing ID")
}
testB64, err := base64.RawURLEncoding.DecodeString(ccr.ID)
if err != nil || !(len(testB64) > 0) {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("ID not base64.RawURLEncoded")
}
if ccr.PublicKeyCredential.Credential.Type == "" {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing type")
}
if ccr.PublicKeyCredential.Credential.Type != "public-key" {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Type not public-key")
}
var pcc ParsedCredentialCreationData
pcc.ID, pcc.RawID, pcc.Type = ccr.ID, ccr.RawID, ccr.Type
pcc.Raw = ccr
parsedAttestationResponse, err := ccr.AttestationResponse.Parse()
if err != nil {
return nil, ErrParsingData.WithDetails("Error parsing attestation response")
}
pcc.Response = *parsedAttestationResponse
return &pcc, nil
}
// Verifies the Client and Attestation data as laid out by §7.1. Registering a new credential
// https://www.w3.org/TR/webauthn/#registering-a-new-credential
func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID, relyingPartyOrigin string) error {
// Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data
verifyError := pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, relyingPartyOrigin)
if verifyError != nil {
return verifyError
}
// Step 7. Compute the hash of response.clientDataJSON using SHA-256.
clientDataHash := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON)
// Step 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
// structure to obtain the attestation statement format fmt, the authenticator data authData, and the
// attestation statement attStmt. is handled while
// We do the above step while parsing and decoding the CredentialCreationResponse
// Handle steps 9 through 14 - This verifies the attestaion object and
verifyError = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash[:], verifyUser)
if verifyError != nil {
return verifyError
}
// Step 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root
// certificates or ECDAA-Issuer public keys) for that attestation type and attestation statement
// format fmt, from a trusted source or from policy. For example, the FIDO Metadata Service provides
// one way to obtain such information, using the aaguid in the attestedCredentialData in authData.
// [https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-metadata-service-v2.0-id-20180227.html]
// TODO: There are no valid AAGUIDs yet or trust sources supported. We could implement policy for the RP in
// the future, however.
// Step 16. Assess the attestation trustworthiness using outputs of the verification procedure in step 14, as follows:
// - If self attestation was used, check if self attestation is acceptable under Relying Party policy.
// - If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in
// the set of acceptable trust anchors obtained in step 15.
// - Otherwise, use the X.509 certificates returned by the verification procedure to verify that the
// attestation public key correctly chains up to an acceptable root certificate.
// TODO: We're not supporting trust anchors, self-attestation policy, or acceptable root certs yet
// Step 17. Check that the credentialId is not yet registered to any other user. If registration is
// requested for a credential that is already registered to a different user, the Relying Party SHOULD
// fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting
// the older registration.
// TODO: We can't support this in the code's current form, the Relying Party would need to check for this
// against their database
// Step 18 If the attestation statement attStmt verified successfully and is found to be trustworthy, then
// register the new credential with the account that was denoted in the options.user passed to create(), by
// associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, as
// appropriate for the Relying Party's system.
// Step 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above,
// the Relying Party SHOULD fail the registration ceremony.
// TODO: Not implemented for the reasons mentioned under Step 16
return nil
}
// GetAppID takes a AuthenticationExtensions object or nil. It then performs the following checks in order:
//
// 1. Check that the Session Data's AuthenticationExtensions has been provided and return a blank appid if it hasn't been.
// 2. Check that the AuthenticationExtensionsClientOutputs contains the extensions output and return a blank appid if it doesn't.
// 3. Check that the Credential AttestationType is `fido-u2f` and return a blank appid if it isn't.
// 4. Check that the AuthenticationExtensionsClientOutputs contains the appid key and return a blank appid if it doesn't.
// 5. Check that the AuthenticationExtensionsClientOutputs appid is a bool and return an error if it isn't.
// 6. Check that the appid output is true and return a blank appid if it isn't.
// 7. Check that the Session Data has an appid extension defined and return an error if it doesn't.
// 8. Check that the appid extension in Session Data is a string and return an error if it isn't.
// 9. Return the appid extension value from the Session Data.
func (ppkc ParsedPublicKeyCredential) GetAppID(authExt AuthenticationExtensions, credentialAttestationType string) (appID string, err error) {
var (
value, clientValue interface{}
enableAppID, ok bool
)
if authExt == nil {
return "", nil
}
if ppkc.ClientExtensionResults == nil {
return "", nil
}
// If the credential does not have the correct attestation type it is assumed to NOT be a fido-u2f credential.
// https://w3c.github.io/webauthn/#sctn-fido-u2f-attestation
if credentialAttestationType != "fido-u2f" {
return "", nil
}
if clientValue, ok = ppkc.ClientExtensionResults["appid"]; !ok {
return "", nil
}
if enableAppID, ok = clientValue.(bool); !ok {
return "", ErrBadRequest.WithDetails("Client Output appid did not have the expected type")
}
if !enableAppID {
return "", nil
}
if value, ok = authExt["appid"]; !ok {
return "", ErrBadRequest.WithDetails("Session Data does not have an appid but Client Output indicates it should be set")
}
if appID, ok = value.(string); !ok {
return "", ErrBadRequest.WithDetails("Session Data appid did not have the expected type")
}
return appID, nil
}
+8
View File
@@ -0,0 +1,8 @@
// The protocol package contains data structures and validation functionality
// outlined in the Web Authnentication specification (https://www.w3.org/TR/webauthn).
// The data structures here attempt to conform as much as possible to their definitions,
// but some structs (like those that are used as part of validation steps) contain
// additional fields that help us unpack and validate the data we unmarshall.
// When implementing this library, most developers will primarily be using the API
// outlined in the webauthn package.
package protocol
+48
View File
@@ -0,0 +1,48 @@
package protocol
// From §5.4.1 (https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity).
// PublicKeyCredentialEntity describes a user account, or a WebAuthn Relying Party,
// with which a public key credential is associated.
type CredentialEntity struct {
// A human-palatable name for the entity. Its function depends on what the PublicKeyCredentialEntity represents:
//
// When inherited by PublicKeyCredentialRpEntity it is a human-palatable identifier for the Relying Party,
// intended only for display. For example, "ACME Corporation", "Wonderful Widgets, Inc." or "ОАО Примертех".
//
// When inherited by PublicKeyCredentialUserEntity, it is a human-palatable identifier for a user account. It is
// intended only for display, i.e., aiding the user in determining the difference between user accounts with similar
// displayNames. For example, "alexm", "alex.p.mueller@example.com" or "+14255551234".
Name string `json:"name"`
// A serialized URL which resolves to an image associated with the entity. For example,
// this could be a users avatar or a Relying Party's logo. This URL MUST be an a priori
// authenticated URL. Authenticators MUST accept and store a 128-byte minimum length for
// an icon members value. Authenticators MAY ignore an icon members value if its length
// is greater than 128 bytes. The URLs scheme MAY be "data" to avoid fetches of the URL,
// at the cost of needing more storage.
Icon string `json:"icon,omitempty"`
}
// From §5.4.2 (https://www.w3.org/TR/webauthn/#sctn-rp-credential-params).
// The PublicKeyCredentialRpEntity is used to supply additional
// Relying Party attributes when creating a new credential.
type RelyingPartyEntity struct {
CredentialEntity
// A unique identifier for the Relying Party entity, which sets the RP ID.
ID string `json:"id"`
}
// From §5.4.3 (https://www.w3.org/TR/webauthn/#sctn-user-credential-params).
// The PublicKeyCredentialUserEntity is used to supply additional
// user account attributes when creating a new credential.
type UserEntity struct {
CredentialEntity
// A human-palatable name for the user account, intended only for display.
// For example, "Alex P. Müller" or "田中 倫". The Relying Party SHOULD let
// the user choose this, and SHOULD NOT restrict the choice more than necessary.
DisplayName string `json:"displayName,omitempty"`
// ID is the user handle of the user account entity. To ensure secure operation,
// authentication and authorization decisions MUST be made on the basis of this id
// member, not the displayName nor name members. See Section 6.1 of
// [RFC8266](https://www.w3.org/TR/webauthn/#biblio-rfc8266).
ID []byte `json:"id"`
}
+85
View File
@@ -0,0 +1,85 @@
package protocol
type Error struct {
// Short name for the type of error that has occurred
Type string `json:"type"`
// Additional details about the error
Details string `json:"error"`
// Information to help debug the error
DevInfo string `json:"debug"`
}
var (
ErrBadRequest = &Error{
Type: "invalid_request",
Details: "Error reading the requst data",
}
ErrChallengeMismatch = &Error{
Type: "challenge_mismatch",
Details: "Stored challenge and received challenge do not match",
}
ErrParsingData = &Error{
Type: "parse_error",
Details: "Error parsing the authenticator response",
}
ErrAuthData = &Error{
Type: "auth_data",
Details: "Error verifying the authenticator data",
}
ErrVerification = &Error{
Type: "verification_error",
Details: "Error validating the authenticator response",
}
ErrAttestation = &Error{
Type: "attesation_error",
Details: "Error validating the attestation data provided",
}
ErrInvalidAttestation = &Error{
Type: "invalid_attestation",
Details: "Invalid attestation data",
}
ErrAttestationFormat = &Error{
Type: "invalid_attestation",
Details: "Invalid attestation format",
}
ErrAttestationCertificate = &Error{
Type: "invalid_certificate",
Details: "Invalid attestation certificate",
}
ErrAssertionSignature = &Error{
Type: "invalid_signature",
Details: "Assertion Signature against auth data and client hash is not valid",
}
ErrUnsupportedKey = &Error{
Type: "invalid_key_type",
Details: "Unsupported Public Key Type",
}
ErrUnsupportedAlgorithm = &Error{
Type: "unsupported_key_algorithm",
Details: "Unsupported public key algorithm",
}
ErrNotSpecImplemented = &Error{
Type: "spec_unimplemented",
Details: "This field is not yet supported by the WebAuthn spec",
}
ErrNotImplemented = &Error{
Type: "not_implemented",
Details: "This field is not yet supported by this library",
}
)
func (err *Error) Error() string {
return err.Details
}
func (passedError *Error) WithDetails(details string) *Error {
err := *passedError
err.Details = details
return &err
}
func (passedError *Error) WithInfo(info string) *Error {
err := *passedError
err.DevInfo = info
return &err
}
+8
View File
@@ -0,0 +1,8 @@
package protocol
// Extensions are discussed in §9. WebAuthn Extensions (https://www.w3.org/TR/webauthn/#extensions).
// For a list of commonly supported extenstions, see §10. Defined Extensions
// (https://www.w3.org/TR/webauthn/#sctn-defined-extensions).
type AuthenticationExtensionsClientOutputs map[string]interface{}
+282
View File
@@ -0,0 +1,282 @@
package googletpm
import (
"bytes"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"fmt"
"hash"
)
// DecodeAttestationData decode a TPMS_ATTEST message. No error is returned if
// the input has extra trailing data.
func DecodeAttestationData(in []byte) (*AttestationData, error) {
buf := bytes.NewBuffer(in)
var ad AttestationData
if err := UnpackBuf(buf, &ad.Magic, &ad.Type); err != nil {
return nil, fmt.Errorf("decoding Magic/Type: %v", err)
}
n, err := decodeName(buf)
if err != nil {
return nil, fmt.Errorf("decoding QualifiedSigner: %v", err)
}
ad.QualifiedSigner = *n
if err := UnpackBuf(buf, &ad.ExtraData, &ad.ClockInfo, &ad.FirmwareVersion); err != nil {
return nil, fmt.Errorf("decoding ExtraData/ClockInfo/FirmwareVersion: %v", err)
}
// The spec specifies several other types of attestation data. We only need
// parsing of Certify & Creation attestation data for now. If you need
// support for other attestation types, add them here.
switch ad.Type {
case TagAttestCertify:
if ad.AttestedCertifyInfo, err = decodeCertifyInfo(buf); err != nil {
return nil, fmt.Errorf("decoding AttestedCertifyInfo: %v", err)
}
case TagAttestCreation:
if ad.AttestedCreationInfo, err = decodeCreationInfo(buf); err != nil {
return nil, fmt.Errorf("decoding AttestedCreationInfo: %v", err)
}
case TagAttestQuote:
if ad.AttestedQuoteInfo, err = decodeQuoteInfo(buf); err != nil {
return nil, fmt.Errorf("decoding AttestedQuoteInfo: %v", err)
}
default:
return nil, fmt.Errorf("only Certify & Creation attestation structures are supported, got type 0x%x", ad.Type)
}
return &ad, nil
}
// AttestationData contains data attested by TPM commands (like Certify).
type AttestationData struct {
Magic uint32
Type Tag
QualifiedSigner Name
ExtraData []byte
ClockInfo ClockInfo
FirmwareVersion uint64
AttestedCertifyInfo *CertifyInfo
AttestedQuoteInfo *QuoteInfo
AttestedCreationInfo *CreationInfo
}
// Tag is a command tag.
type Tag uint16
type Name struct {
Handle *Handle
Digest *HashValue
}
// A Handle is a reference to a TPM object.
type Handle uint32
type HashValue struct {
Alg Algorithm
Value []byte
}
// ClockInfo contains TPM state info included in AttestationData.
type ClockInfo struct {
Clock uint64
ResetCount uint32
RestartCount uint32
Safe byte
}
// CertifyInfo contains Certify-specific data for TPMS_ATTEST.
type CertifyInfo struct {
Name Name
QualifiedName Name
}
// QuoteInfo represents a TPMS_QUOTE_INFO structure.
type QuoteInfo struct {
PCRSelection PCRSelection
PCRDigest []byte
}
// PCRSelection contains a slice of PCR indexes and a hash algorithm used in
// them.
type PCRSelection struct {
Hash Algorithm
PCRs []int
}
// CreationInfo contains Creation-specific data for TPMS_ATTEST.
type CreationInfo struct {
Name Name
// Most TPM2B_Digest structures contain a TPMU_HA structure
// and get parsed to HashValue. This is never the case for the
// digest in TPMS_CREATION_INFO.
OpaqueDigest []byte
}
func decodeName(in *bytes.Buffer) (*Name, error) {
var nameBuf []byte
if err := UnpackBuf(in, &nameBuf); err != nil {
return nil, err
}
name := new(Name)
switch len(nameBuf) {
case 0:
// No name is present.
case 4:
name.Handle = new(Handle)
if err := UnpackBuf(bytes.NewBuffer(nameBuf), name.Handle); err != nil {
return nil, fmt.Errorf("decoding Handle: %v", err)
}
default:
var err error
name.Digest, err = decodeHashValue(bytes.NewBuffer(nameBuf))
if err != nil {
return nil, fmt.Errorf("decoding Digest: %v", err)
}
}
return name, nil
}
func decodeHashValue(in *bytes.Buffer) (*HashValue, error) {
var hv HashValue
if err := UnpackBuf(in, &hv.Alg); err != nil {
return nil, fmt.Errorf("decoding Alg: %v", err)
}
hfn, ok := hashConstructors[hv.Alg]
if !ok {
return nil, fmt.Errorf("unsupported hash algorithm type 0x%x", hv.Alg)
}
hv.Value = make([]byte, hfn().Size())
if _, err := in.Read(hv.Value); err != nil {
return nil, fmt.Errorf("decoding Value: %v", err)
}
return &hv, nil
}
// HashConstructor returns a function that can be used to make a
// hash.Hash using the specified algorithm. An error is returned
// if the algorithm is not a hash algorithm.
func (a Algorithm) HashConstructor() (func() hash.Hash, error) {
c, ok := hashConstructors[a]
if !ok {
return nil, fmt.Errorf("algorithm not supported: 0x%x", a)
}
return c, nil
}
var hashConstructors = map[Algorithm]func() hash.Hash{
AlgSHA1: sha1.New,
AlgSHA256: sha256.New,
AlgSHA384: sha512.New384,
AlgSHA512: sha512.New,
}
// TPM Structure Tags. Tags are used to disambiguate structures, similar to Alg
// values: tag value defines what kind of data lives in a nested field.
const (
TagNull Tag = 0x8000
TagNoSessions Tag = 0x8001
TagSessions Tag = 0x8002
TagAttestCertify Tag = 0x8017
TagAttestQuote Tag = 0x8018
TagAttestCreation Tag = 0x801a
TagHashCheck Tag = 0x8024
)
func decodeCertifyInfo(in *bytes.Buffer) (*CertifyInfo, error) {
var ci CertifyInfo
n, err := decodeName(in)
if err != nil {
return nil, fmt.Errorf("decoding Name: %v", err)
}
ci.Name = *n
n, err = decodeName(in)
if err != nil {
return nil, fmt.Errorf("decoding QualifiedName: %v", err)
}
ci.QualifiedName = *n
return &ci, nil
}
func decodeCreationInfo(in *bytes.Buffer) (*CreationInfo, error) {
var ci CreationInfo
n, err := decodeName(in)
if err != nil {
return nil, fmt.Errorf("decoding Name: %v", err)
}
ci.Name = *n
if err := UnpackBuf(in, &ci.OpaqueDigest); err != nil {
return nil, fmt.Errorf("decoding Digest: %v", err)
}
return &ci, nil
}
func decodeQuoteInfo(in *bytes.Buffer) (*QuoteInfo, error) {
var out QuoteInfo
sel, err := decodeTPMLPCRSelection(in)
if err != nil {
return nil, fmt.Errorf("decoding PCRSelection: %v", err)
}
out.PCRSelection = sel
if err := UnpackBuf(in, &out.PCRDigest); err != nil {
return nil, fmt.Errorf("decoding PCRDigest: %v", err)
}
return &out, nil
}
func decodeTPMLPCRSelection(buf *bytes.Buffer) (PCRSelection, error) {
var count uint32
var sel PCRSelection
if err := UnpackBuf(buf, &count); err != nil {
return sel, err
}
switch count {
case 0:
sel.Hash = AlgUnknown
return sel, nil
case 1: // We only support decoding of a single PCRSelection.
default:
return sel, fmt.Errorf("decoding TPML_PCR_SELECTION list longer than 1 is not supported (got length %d)", count)
}
// See comment in encodeTPMLPCRSelection for details on this format.
var ts tpmsPCRSelection
if err := UnpackBuf(buf, &ts.Hash, &ts.Size); err != nil {
return sel, err
}
ts.PCRs = make([]byte, ts.Size)
if _, err := buf.Read(ts.PCRs); err != nil {
return sel, err
}
sel.Hash = ts.Hash
for i := 0; i < int(ts.Size); i++ {
for j := 0; j < 8; j++ {
set := ts.PCRs[i] & byte(1<<byte(j))
if set == 0 {
continue
}
sel.PCRs = append(sel.PCRs, 8*i+j)
}
}
return sel, nil
}
type tpmsPCRSelection struct {
Hash Algorithm
Size byte
PCRs RawBytes
}
// RawBytes is for Pack and RunCommand arguments that are already encoded.
// Compared to []byte, RawBytes will not be prepended with slice length during
// encoding.
type RawBytes []byte
+152
View File
@@ -0,0 +1,152 @@
package googletpm
import (
"encoding/binary"
"errors"
"fmt"
"io"
"reflect"
)
// From github.com/google/go-tpm
// Portions of existing package conflicted with existing build environment
// and only needed very small amount of code for pubarea and certinfo structs
// so copied them out to this package
// Supported Algorithms.
const (
AlgUnknown Algorithm = 0x0000
AlgRSA Algorithm = 0x0001
AlgSHA1 Algorithm = 0x0004
AlgAES Algorithm = 0x0006
AlgKeyedHash Algorithm = 0x0008
AlgSHA256 Algorithm = 0x000B
AlgSHA384 Algorithm = 0x000C
AlgSHA512 Algorithm = 0x000D
AlgNull Algorithm = 0x0010
AlgRSASSA Algorithm = 0x0014
AlgRSAES Algorithm = 0x0015
AlgRSAPSS Algorithm = 0x0016
AlgOAEP Algorithm = 0x0017
AlgECDSA Algorithm = 0x0018
AlgECDH Algorithm = 0x0019
AlgECDAA Algorithm = 0x001A
AlgKDF2 Algorithm = 0x0021
AlgECC Algorithm = 0x0023
AlgCTR Algorithm = 0x0040
AlgOFB Algorithm = 0x0041
AlgCBC Algorithm = 0x0042
AlgCFB Algorithm = 0x0043
AlgECB Algorithm = 0x0044
)
// UnpackBuf recursively unpacks types from a reader just as encoding/binary
// does under binary.BigEndian, but with one difference: it unpacks a byte
// slice by first reading an integer with lengthPrefixSize bytes, then reading
// that many bytes. It assumes that incoming values are pointers to values so
// that, e.g., underlying slices can be resized as needed.
func UnpackBuf(buf io.Reader, elts ...interface{}) error {
for _, e := range elts {
v := reflect.ValueOf(e)
k := v.Kind()
if k != reflect.Ptr {
return fmt.Errorf("all values passed to Unpack must be pointers, got %v", k)
}
if v.IsNil() {
return errors.New("can't fill a nil pointer")
}
iv := reflect.Indirect(v)
switch iv.Kind() {
case reflect.Struct:
// Decompose the struct and copy over the values.
for i := 0; i < iv.NumField(); i++ {
if err := UnpackBuf(buf, iv.Field(i).Addr().Interface()); err != nil {
return err
}
}
case reflect.Slice:
var size int
_, isHandles := e.(*[]Handle)
switch {
// []Handle always uses 2-byte length, even with TPM 1.2.
case isHandles:
var tmpSize uint16
if err := binary.Read(buf, binary.BigEndian, &tmpSize); err != nil {
return err
}
size = int(tmpSize)
// TPM 2.0
case lengthPrefixSize == tpm20PrefixSize:
var tmpSize uint16
if err := binary.Read(buf, binary.BigEndian, &tmpSize); err != nil {
return err
}
size = int(tmpSize)
// TPM 1.2
case lengthPrefixSize == tpm12PrefixSize:
var tmpSize uint32
if err := binary.Read(buf, binary.BigEndian, &tmpSize); err != nil {
return err
}
size = int(tmpSize)
default:
return fmt.Errorf("lengthPrefixSize is %d, must be either 2 or 4", lengthPrefixSize)
}
// A zero size is used by the TPM to signal that certain elements
// are not present.
if size == 0 {
continue
}
// Make len(e) match size exactly.
switch b := e.(type) {
case *[]byte:
if len(*b) >= size {
*b = (*b)[:size]
} else {
*b = append(*b, make([]byte, size-len(*b))...)
}
case *[]Handle:
if len(*b) >= size {
*b = (*b)[:size]
} else {
*b = append(*b, make([]Handle, size-len(*b))...)
}
default:
return fmt.Errorf("can't fill pointer to %T, only []byte or []Handle slices", e)
}
if err := binary.Read(buf, binary.BigEndian, e); err != nil {
return err
}
default:
if err := binary.Read(buf, binary.BigEndian, e); err != nil {
return err
}
}
}
return nil
}
// lengthPrefixSize is the size in bytes of length prefix for byte slices.
//
// In TPM 1.2 this is 4 bytes.
// In TPM 2.0 this is 2 bytes.
var lengthPrefixSize int
const (
tpm12PrefixSize = 4
tpm20PrefixSize = 2
)
// UseTPM20LengthPrefixSize makes Pack/Unpack use TPM 2.0 encoding for byte
// arrays.
func UseTPM20LengthPrefixSize() {
lengthPrefixSize = tpm20PrefixSize
}
+240
View File
@@ -0,0 +1,240 @@
package googletpm
import (
"bytes"
"fmt"
"math/big"
)
// DecodePublic decodes a TPMT_PUBLIC message. No error is returned if
// the input has extra trailing data.
func DecodePublic(buf []byte) (Public, error) {
in := bytes.NewBuffer(buf)
var pub Public
var err error
if err = UnpackBuf(in, &pub.Type, &pub.NameAlg, &pub.Attributes, &pub.AuthPolicy); err != nil {
return pub, fmt.Errorf("decoding TPMT_PUBLIC: %v", err)
}
switch pub.Type {
case AlgRSA:
pub.RSAParameters, err = decodeRSAParams(in)
case AlgECC:
pub.ECCParameters, err = decodeECCParams(in)
default:
err = fmt.Errorf("unsupported type in TPMT_PUBLIC: %v", pub.Type)
}
return pub, err
}
// Public contains the public area of an object.
type Public struct {
Type Algorithm
NameAlg Algorithm
Attributes KeyProp
AuthPolicy []byte
// If Type is AlgKeyedHash, then do not set these.
// Otherwise, only one of the Parameters fields should be set. When encoding/decoding,
// one will be picked based on Type.
RSAParameters *RSAParams
ECCParameters *ECCParams
}
// Algorithm represents a TPM_ALG_ID value.
type Algorithm uint16
// KeyProp is a bitmask used in Attributes field of key templates. Individual
// flags should be OR-ed to form a full mask.
type KeyProp uint32
// Key properties.
const (
FlagFixedTPM KeyProp = 0x00000002
FlagFixedParent KeyProp = 0x00000010
FlagSensitiveDataOrigin KeyProp = 0x00000020
FlagUserWithAuth KeyProp = 0x00000040
FlagAdminWithPolicy KeyProp = 0x00000080
FlagNoDA KeyProp = 0x00000400
FlagRestricted KeyProp = 0x00010000
FlagDecrypt KeyProp = 0x00020000
FlagSign KeyProp = 0x00040000
FlagSealDefault = FlagFixedTPM | FlagFixedParent
FlagSignerDefault = FlagSign | FlagRestricted | FlagFixedTPM |
FlagFixedParent | FlagSensitiveDataOrigin | FlagUserWithAuth
FlagStorageDefault = FlagDecrypt | FlagRestricted | FlagFixedTPM |
FlagFixedParent | FlagSensitiveDataOrigin | FlagUserWithAuth
)
func decodeRSAParams(in *bytes.Buffer) (*RSAParams, error) {
var params RSAParams
var err error
if params.Symmetric, err = decodeSymScheme(in); err != nil {
return nil, fmt.Errorf("decoding Symmetric: %v", err)
}
if params.Sign, err = decodeSigScheme(in); err != nil {
return nil, fmt.Errorf("decoding Sign: %v", err)
}
var modBytes []byte
if err := UnpackBuf(in, &params.KeyBits, &params.Exponent, &modBytes); err != nil {
return nil, fmt.Errorf("decoding KeyBits, Exponent, Modulus: %v", err)
}
if params.Exponent == 0 {
params.encodeDefaultExponentAsZero = true
params.Exponent = defaultRSAExponent
}
params.Modulus = new(big.Int).SetBytes(modBytes)
return &params, nil
}
const defaultRSAExponent = 1<<16 + 1
// RSAParams represents parameters of an RSA key pair.
//
// Symmetric and Sign may be nil, depending on key Attributes in Public.
//
// One of Modulus and ModulusRaw must always be non-nil. Modulus takes
// precedence. ModulusRaw is used for key templates where the field named
// "unique" must be a byte array of all zeroes.
type RSAParams struct {
Symmetric *SymScheme
Sign *SigScheme
KeyBits uint16
// The default Exponent (65537) has two representations; the
// 0 value, and the value 65537.
// If encodeDefaultExponentAsZero is set, an exponent of 65537
// will be encoded as zero. This is necessary to produce an identical
// encoded bitstream, so Name digest calculations will be correct.
encodeDefaultExponentAsZero bool
Exponent uint32
ModulusRaw []byte
Modulus *big.Int
}
// SymScheme represents a symmetric encryption scheme.
type SymScheme struct {
Alg Algorithm
KeyBits uint16
Mode Algorithm
} // SigScheme represents a signing scheme.
type SigScheme struct {
Alg Algorithm
Hash Algorithm
Count uint32
}
func decodeSigScheme(in *bytes.Buffer) (*SigScheme, error) {
var scheme SigScheme
if err := UnpackBuf(in, &scheme.Alg); err != nil {
return nil, fmt.Errorf("decoding Alg: %v", err)
}
if scheme.Alg == AlgNull {
return nil, nil
}
if err := UnpackBuf(in, &scheme.Hash); err != nil {
return nil, fmt.Errorf("decoding Hash: %v", err)
}
if scheme.Alg.UsesCount() {
if err := UnpackBuf(in, &scheme.Count); err != nil {
return nil, fmt.Errorf("decoding Count: %v", err)
}
}
return &scheme, nil
}
// UsesCount returns true if a signature algorithm uses count value.
func (a Algorithm) UsesCount() bool {
return a == AlgECDAA
}
func decodeKDFScheme(in *bytes.Buffer) (*KDFScheme, error) {
var scheme KDFScheme
if err := UnpackBuf(in, &scheme.Alg); err != nil {
return nil, fmt.Errorf("decoding Alg: %v", err)
}
if scheme.Alg == AlgNull {
return nil, nil
}
if err := UnpackBuf(in, &scheme.Hash); err != nil {
return nil, fmt.Errorf("decoding Hash: %v", err)
}
return &scheme, nil
}
func decodeSymScheme(in *bytes.Buffer) (*SymScheme, error) {
var scheme SymScheme
if err := UnpackBuf(in, &scheme.Alg); err != nil {
return nil, fmt.Errorf("decoding Alg: %v", err)
}
if scheme.Alg == AlgNull {
return nil, nil
}
if err := UnpackBuf(in, &scheme.KeyBits, &scheme.Mode); err != nil {
return nil, fmt.Errorf("decoding KeyBits, Mode: %v", err)
}
return &scheme, nil
}
func decodeECCParams(in *bytes.Buffer) (*ECCParams, error) {
var params ECCParams
var err error
if params.Symmetric, err = decodeSymScheme(in); err != nil {
return nil, fmt.Errorf("decoding Symmetric: %v", err)
}
if params.Sign, err = decodeSigScheme(in); err != nil {
return nil, fmt.Errorf("decoding Sign: %v", err)
}
if err := UnpackBuf(in, &params.CurveID); err != nil {
return nil, fmt.Errorf("decoding CurveID: %v", err)
}
if params.KDF, err = decodeKDFScheme(in); err != nil {
return nil, fmt.Errorf("decoding KDF: %v", err)
}
var x, y []byte
if err := UnpackBuf(in, &x, &y); err != nil {
return nil, fmt.Errorf("decoding Point: %v", err)
}
params.Point.X = new(big.Int).SetBytes(x)
params.Point.Y = new(big.Int).SetBytes(y)
return &params, nil
}
// ECCParams represents parameters of an ECC key pair.
//
// Symmetric, Sign and KDF may be nil, depending on key Attributes in Public.
type ECCParams struct {
Symmetric *SymScheme
Sign *SigScheme
CurveID EllipticCurve
KDF *KDFScheme
Point ECPoint
}
// EllipticCurve identifies specific EC curves.
type EllipticCurve uint16
// ECC curves supported by TPM 2.0 spec.
const (
CurveNISTP192 = EllipticCurve(iota + 1)
CurveNISTP224
CurveNISTP256
CurveNISTP384
CurveNISTP521
CurveBNP256 = EllipticCurve(iota + 10)
CurveBNP638
CurveSM2P256 = EllipticCurve(0x0020)
)
// ECPoint represents a ECC coordinates for a point.
type ECPoint struct {
X, Y *big.Int
}
// KDFScheme represents a KDF (Key Derivation Function) scheme.
type KDFScheme struct {
Alg Algorithm
Hash Algorithm
}
+136
View File
@@ -0,0 +1,136 @@
package protocol
import (
"github.com/duo-labs/webauthn/protocol/webauthncose"
)
type CredentialCreation struct {
Response PublicKeyCredentialCreationOptions `json:"publicKey"`
}
type CredentialAssertion struct {
Response PublicKeyCredentialRequestOptions `json:"publicKey"`
}
// In order to create a Credential via create(), the caller specifies a few parameters in a CredentialCreationOptions object.
// See §5.4. Options for Credential Creation https://www.w3.org/TR/webauthn/#dictionary-makecredentialoptions
type PublicKeyCredentialCreationOptions struct {
Challenge Challenge `json:"challenge"`
RelyingParty RelyingPartyEntity `json:"rp"`
User UserEntity `json:"user"`
Parameters []CredentialParameter `json:"pubKeyCredParams,omitempty"`
AuthenticatorSelection AuthenticatorSelection `json:"authenticatorSelection,omitempty"`
Timeout int `json:"timeout,omitempty"`
CredentialExcludeList []CredentialDescriptor `json:"excludeCredentials,omitempty"`
Extensions AuthenticationExtensions `json:"extensions,omitempty"`
Attestation ConveyancePreference `json:"attestation,omitempty"`
}
// The PublicKeyCredentialRequestOptions dictionary supplies get() with the data it needs to generate an assertion.
// Its challenge member MUST be present, while its other members are OPTIONAL.
// See §5.5. Options for Assertion Generation https://www.w3.org/TR/webauthn/#assertion-options
type PublicKeyCredentialRequestOptions struct {
Challenge Challenge `json:"challenge"`
Timeout int `json:"timeout,omitempty"`
RelyingPartyID string `json:"rpId,omitempty"`
AllowedCredentials []CredentialDescriptor `json:"allowCredentials,omitempty"`
UserVerification UserVerificationRequirement `json:"userVerification,omitempty"` // Default is "preferred"
Extensions AuthenticationExtensions `json:"extensions,omitempty"`
}
// This dictionary contains the attributes that are specified by a caller when referring to a public
// key credential as an input parameter to the create() or get() methods. It mirrors the fields of
// the PublicKeyCredential object returned by the latter methods.
// See §5.10.3. Credential Descriptor https://www.w3.org/TR/webauthn/#credential-dictionary
type CredentialDescriptor struct {
// The valid credential types.
Type CredentialType `json:"type"`
// CredentialID The ID of a credential to allow/disallow
CredentialID []byte `json:"id"`
// The authenticator transports that can be used
Transport []AuthenticatorTransport `json:"transports,omitempty"`
}
// CredentialParameter is the credential type and algorithm
// that the relying party wants the authenticator to create
type CredentialParameter struct {
Type CredentialType `json:"type"`
Algorithm webauthncose.COSEAlgorithmIdentifier `json:"alg"`
}
// This enumeration defines the valid credential types.
// It is an extension point; values can be added to it in the future, as
// more credential types are defined. The values of this enumeration are used
// for versioning the Authentication Assertion and attestation structures according
// to the type of the authenticator.
// See §5.10.3. Credential Descriptor https://www.w3.org/TR/webauthn/#credentialType
type CredentialType string
const (
// PublicKeyCredentialType - Currently one credential type is defined, namely "public-key".
PublicKeyCredentialType CredentialType = "public-key"
)
// AuthenticationExtensions - referred to as AuthenticationExtensionsClientInputs in the
// spec document, this member contains additional parameters requesting additional processing
// by the client and authenticator.
// This is currently under development
type AuthenticationExtensions map[string]interface{}
// WebAuthn Relying Parties may use the AuthenticatorSelectionCriteria dictionary to specify their requirements
// regarding authenticator attributes. See §5.4.4. Authenticator Selection Criteria
// https://www.w3.org/TR/webauthn/#authenticatorSelection
type AuthenticatorSelection struct {
// AuthenticatorAttachment If this member is present, eligible authenticators are filtered to only
// authenticators attached with the specified AuthenticatorAttachment enum
AuthenticatorAttachment AuthenticatorAttachment `json:"authenticatorAttachment,omitempty"`
// RequireResidentKey this member describes the Relying Party's requirements regarding resident
// credentials. If the parameter is set to true, the authenticator MUST create a client-side-resident
// public key credential source when creating a public key credential.
RequireResidentKey *bool `json:"requireResidentKey,omitempty"`
// UserVerification This member describes the Relying Party's requirements regarding user verification for
// the create() operation. Eligible authenticators are filtered to only those capable of satisfying this
// requirement.
UserVerification UserVerificationRequirement `json:"userVerification,omitempty"`
}
// WebAuthn Relying Parties may use AttestationConveyancePreference to specify their preference regarding
// attestation conveyance during credential generation. See §5.4.6. https://www.w3.org/TR/webauthn/#attestation-convey
type ConveyancePreference string
const (
// The default value. This value indicates that the Relying Party is not interested in authenticator attestation. For example,
// in order to potentially avoid having to obtain user consent to relay identifying information to the Relying Party, or to
// save a roundtrip to an Attestation CA.
PreferNoAttestation ConveyancePreference = "none"
// This value indicates that the Relying Party prefers an attestation conveyance yielding verifiable attestation
// statements, but allows the client to decide how to obtain such attestation statements. The client MAY replace
// the authenticator-generated attestation statements with attestation statements generated by an Anonymization
// CA, in order to protect the users privacy, or to assist Relying Parties with attestation verification in a
// heterogeneous ecosystem.
PreferIndirectAttestation ConveyancePreference = "indirect"
// This value indicates that the Relying Party wants to receive the attestation statement as generated by the authenticator.
PreferDirectAttestation ConveyancePreference = "direct"
)
func (a *PublicKeyCredentialRequestOptions) GetAllowedCredentialIDs() [][]byte {
var allowedCredentialIDs = make([][]byte, len(a.AllowedCredentials))
for i, credential := range a.AllowedCredentials {
allowedCredentialIDs[i] = credential.CredentialID
}
return allowedCredentialIDs
}
type Extensions interface{}
type ServerResponse struct {
Status ServerResponseStatus `json:"status"`
Message string `json:"errorMessage"`
}
type ServerResponseStatus string
const (
StatusOk ServerResponseStatus = "ok"
StatusFailed ServerResponseStatus = "failed"
)
+1
View File
@@ -0,0 +1 @@
package protocol
+12
View File
@@ -0,0 +1,12 @@
// +build go1.13
package webauthncose
import (
"crypto/ed25519"
"crypto/x509"
)
func marshalEd25519PublicKey(pub ed25519.PublicKey) ([]byte, error) {
return x509.MarshalPKIXPublicKey(pub)
}
@@ -0,0 +1,38 @@
// +build !go1.13
package webauthncose
import (
"crypto/x509/pkix"
"encoding/asn1"
"golang.org/x/crypto/ed25519"
)
var oidSignatureEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112}
type pkixPublicKey struct {
Algo pkix.AlgorithmIdentifier
BitString asn1.BitString
}
// marshalEd25519PublicKey is a backport of the functionality introduced in
// Go v1.13.
// Ref: https://golang.org/doc/go1.13#crypto/ed25519
// Ref: https://golang.org/doc/go1.13#crypto/x509
func marshalEd25519PublicKey(pub ed25519.PublicKey) ([]byte, error) {
publicKeyBytes := pub
var publicKeyAlgorithm pkix.AlgorithmIdentifier
publicKeyAlgorithm.Algorithm = oidSignatureEd25519
pkix := pkixPublicKey{
Algo: publicKeyAlgorithm,
BitString: asn1.BitString{
Bytes: publicKeyBytes,
BitLength: 8 * len(publicKeyBytes),
},
}
ret, _ := asn1.Marshal(pkix)
return ret, nil
}
@@ -0,0 +1,400 @@
package webauthncose
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"fmt"
"hash"
"math/big"
"github.com/fxamacker/cbor/v2"
"golang.org/x/crypto/ed25519"
)
// PublicKeyData The public key portion of a Relying Party-specific credential key pair, generated
// by an authenticator and returned to a Relying Party at registration time. We unpack this object
// using fxamacker's cbor library ("github.com/fxamacker/cbor/v2") which is why there are cbor tags
// included. The tag field values correspond to the IANA COSE keys that give their respective
// values.
// See §6.4.1.1 https://www.w3.org/TR/webauthn/#sctn-encoded-credPubKey-examples for examples of this
// COSE data.
type PublicKeyData struct {
// Decode the results to int by default.
_struct bool `cbor:",keyasint" json:"public_key"`
// The type of key created. Should be OKP, EC2, or RSA.
KeyType int64 `cbor:"1,keyasint" json:"kty"`
// A COSEAlgorithmIdentifier for the algorithm used to derive the key signature.
Algorithm int64 `cbor:"3,keyasint" json:"alg"`
}
type EC2PublicKeyData struct {
PublicKeyData
// If the key type is EC2, the curve on which we derive the signature from.
Curve int64 `cbor:"-1,keyasint,omitempty" json:"crv"`
// A byte string 32 bytes in length that holds the x coordinate of the key.
XCoord []byte `cbor:"-2,keyasint,omitempty" json:"x"`
// A byte string 32 bytes in length that holds the y coordinate of the key.
YCoord []byte `cbor:"-3,keyasint,omitempty" json:"y"`
}
type RSAPublicKeyData struct {
PublicKeyData
// Represents the modulus parameter for the RSA algorithm
Modulus []byte `cbor:"-1,keyasint,omitempty" json:"n"`
// Represents the exponent parameter for the RSA algorithm
Exponent []byte `cbor:"-2,keyasint,omitempty" json:"e"`
}
type OKPPublicKeyData struct {
PublicKeyData
Curve int64
// A byte string that holds the x coordinate of the key.
XCoord []byte `cbor:"-2,keyasint,omitempty" json:"x"`
}
// Verify Octet Key Pair (OKP) Public Key Signature
func (k *OKPPublicKeyData) Verify(data []byte, sig []byte) (bool, error) {
var key ed25519.PublicKey = make([]byte, ed25519.PublicKeySize)
copy(key, k.XCoord)
return ed25519.Verify(key, data, sig), nil
}
// Verify Elliptic Curce Public Key Signature
func (k *EC2PublicKeyData) Verify(data []byte, sig []byte) (bool, error) {
var curve elliptic.Curve
switch COSEAlgorithmIdentifier(k.Algorithm) {
case AlgES512: // IANA COSE code for ECDSA w/ SHA-512
curve = elliptic.P521()
case AlgES384: // IANA COSE code for ECDSA w/ SHA-384
curve = elliptic.P384()
case AlgES256: // IANA COSE code for ECDSA w/ SHA-256
curve = elliptic.P256()
default:
return false, ErrUnsupportedAlgorithm
}
pubkey := &ecdsa.PublicKey{
Curve: curve,
X: big.NewInt(0).SetBytes(k.XCoord),
Y: big.NewInt(0).SetBytes(k.YCoord),
}
type ECDSASignature struct {
R, S *big.Int
}
e := &ECDSASignature{}
f := HasherFromCOSEAlg(COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm))
h := f()
h.Write(data)
_, err := asn1.Unmarshal(sig, e)
if err != nil {
return false, ErrSigNotProvidedOrInvalid
}
return ecdsa.Verify(pubkey, h.Sum(nil), e.R, e.S), nil
}
// Verify RSA Public Key Signature
func (k *RSAPublicKeyData) Verify(data []byte, sig []byte) (bool, error) {
pubkey := &rsa.PublicKey{
N: big.NewInt(0).SetBytes(k.Modulus),
E: int(uint(k.Exponent[2]) | uint(k.Exponent[1])<<8 | uint(k.Exponent[0])<<16),
}
f := HasherFromCOSEAlg(COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm))
h := f()
h.Write(data)
var hash crypto.Hash
switch COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm) {
case AlgRS1:
hash = crypto.SHA1
case AlgPS256, AlgRS256:
hash = crypto.SHA256
case AlgPS384, AlgRS384:
hash = crypto.SHA384
case AlgPS512, AlgRS512:
hash = crypto.SHA512
default:
return false, ErrUnsupportedAlgorithm
}
switch COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm) {
case AlgPS256, AlgPS384, AlgPS512:
err := rsa.VerifyPSS(pubkey, hash, h.Sum(nil), sig, nil)
return err == nil, err
case AlgRS1, AlgRS256, AlgRS384, AlgRS512:
err := rsa.VerifyPKCS1v15(pubkey, hash, h.Sum(nil), sig)
return err == nil, err
default:
return false, ErrUnsupportedAlgorithm
}
}
// Return which signature algorithm is being used from the COSE Key
func SigAlgFromCOSEAlg(coseAlg COSEAlgorithmIdentifier) SignatureAlgorithm {
for _, details := range SignatureAlgorithmDetails {
if details.coseAlg == coseAlg {
return details.algo
}
}
return UnknownSignatureAlgorithm
}
// Return the Hashing interface to be used for a given COSE Algorithm
func HasherFromCOSEAlg(coseAlg COSEAlgorithmIdentifier) func() hash.Hash {
for _, details := range SignatureAlgorithmDetails {
if details.coseAlg == coseAlg {
return details.hasher
}
}
// default to SHA256? Why not.
return crypto.SHA256.New
}
// Figure out what kind of COSE material was provided and create the data for the new key
func ParsePublicKey(keyBytes []byte) (interface{}, error) {
pk := PublicKeyData{}
cbor.Unmarshal(keyBytes, &pk)
switch COSEKeyType(pk.KeyType) {
case OctetKey:
var o OKPPublicKeyData
cbor.Unmarshal(keyBytes, &o)
o.PublicKeyData = pk
return o, nil
case EllipticKey:
var e EC2PublicKeyData
cbor.Unmarshal(keyBytes, &e)
e.PublicKeyData = pk
return e, nil
case RSAKey:
var r RSAPublicKeyData
cbor.Unmarshal(keyBytes, &r)
r.PublicKeyData = pk
return r, nil
default:
return nil, ErrUnsupportedKey
}
}
// ParseFIDOPublicKey is only used when the appID extension is configured by the assertion response.
func ParseFIDOPublicKey(keyBytes []byte) (EC2PublicKeyData, error) {
x, y := elliptic.Unmarshal(elliptic.P256(), keyBytes)
return EC2PublicKeyData{
PublicKeyData: PublicKeyData{
Algorithm: int64(AlgES256),
},
XCoord: x.Bytes(),
YCoord: y.Bytes(),
}, nil
}
// COSEAlgorithmIdentifier From §5.10.5. A number identifying a cryptographic algorithm. The algorithm
// identifiers SHOULD be values registered in the IANA COSE Algorithms registry
// [https://www.w3.org/TR/webauthn/#biblio-iana-cose-algs-reg], for instance, -7 for "ES256"
// and -257 for "RS256".
type COSEAlgorithmIdentifier int
const (
// AlgES256 ECDSA with SHA-256
AlgES256 COSEAlgorithmIdentifier = -7
// AlgES384 ECDSA with SHA-384
AlgES384 COSEAlgorithmIdentifier = -35
// AlgES512 ECDSA with SHA-512
AlgES512 COSEAlgorithmIdentifier = -36
// AlgRS1 RSASSA-PKCS1-v1_5 with SHA-1
AlgRS1 COSEAlgorithmIdentifier = -65535
// AlgRS256 RSASSA-PKCS1-v1_5 with SHA-256
AlgRS256 COSEAlgorithmIdentifier = -257
// AlgRS384 RSASSA-PKCS1-v1_5 with SHA-384
AlgRS384 COSEAlgorithmIdentifier = -258
// AlgRS512 RSASSA-PKCS1-v1_5 with SHA-512
AlgRS512 COSEAlgorithmIdentifier = -259
// AlgPS256 RSASSA-PSS with SHA-256
AlgPS256 COSEAlgorithmIdentifier = -37
// AlgPS384 RSASSA-PSS with SHA-384
AlgPS384 COSEAlgorithmIdentifier = -38
// AlgPS512 RSASSA-PSS with SHA-512
AlgPS512 COSEAlgorithmIdentifier = -39
// AlgEdDSA EdDSA
AlgEdDSA COSEAlgorithmIdentifier = -8
)
// The Key Type derived from the IANA COSE AuthData
type COSEKeyType int
const (
// OctetKey is an Octet Key
OctetKey COSEKeyType = 1
// EllipticKey is an Elliptic Curve Public Key
EllipticKey COSEKeyType = 2
// RSAKey is an RSA Public Key
RSAKey COSEKeyType = 3
)
func VerifySignature(key interface{}, data []byte, sig []byte) (bool, error) {
switch key.(type) {
case OKPPublicKeyData:
o := key.(OKPPublicKeyData)
return o.Verify(data, sig)
case EC2PublicKeyData:
e := key.(EC2PublicKeyData)
return e.Verify(data, sig)
case RSAPublicKeyData:
r := key.(RSAPublicKeyData)
return r.Verify(data, sig)
default:
return false, ErrUnsupportedKey
}
}
func DisplayPublicKey(cpk []byte) string {
parsedKey, err := ParsePublicKey(cpk)
if err != nil {
return "Cannot display key"
}
switch parsedKey.(type) {
case RSAPublicKeyData:
pKey := parsedKey.(RSAPublicKeyData)
rKey := &rsa.PublicKey{
N: big.NewInt(0).SetBytes(pKey.Modulus),
E: int(uint(pKey.Exponent[2]) | uint(pKey.Exponent[1])<<8 | uint(pKey.Exponent[0])<<16),
}
data, err := x509.MarshalPKIXPublicKey(rKey)
if err != nil {
return "Cannot display key"
}
pemBytes := pem.EncodeToMemory(&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: data,
})
return fmt.Sprintf("%s", pemBytes)
case EC2PublicKeyData:
pKey := parsedKey.(EC2PublicKeyData)
var curve elliptic.Curve
switch COSEAlgorithmIdentifier(pKey.Algorithm) {
case AlgES256:
curve = elliptic.P256()
case AlgES384:
curve = elliptic.P384()
case AlgES512:
curve = elliptic.P521()
default:
return "Cannot display key"
}
eKey := &ecdsa.PublicKey{
Curve: curve,
X: big.NewInt(0).SetBytes(pKey.XCoord),
Y: big.NewInt(0).SetBytes(pKey.YCoord),
}
data, err := x509.MarshalPKIXPublicKey(eKey)
if err != nil {
return "Cannot display key"
}
pemBytes := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: data,
})
return fmt.Sprintf("%s", pemBytes)
case OKPPublicKeyData:
pKey := parsedKey.(OKPPublicKeyData)
if len(pKey.XCoord) != ed25519.PublicKeySize {
return "Cannot display key"
}
var oKey ed25519.PublicKey = make([]byte, ed25519.PublicKeySize)
copy(oKey, pKey.XCoord)
data, err := marshalEd25519PublicKey(oKey)
if err != nil {
return "Cannot display key"
}
pemBytes := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: data,
})
return fmt.Sprintf("%s", pemBytes)
default:
return "Cannot display key of this type"
}
}
// Algorithm enumerations used for
type SignatureAlgorithm int
const (
UnknownSignatureAlgorithm SignatureAlgorithm = iota
MD2WithRSA
MD5WithRSA
SHA1WithRSA
SHA256WithRSA
SHA384WithRSA
SHA512WithRSA
DSAWithSHA1
DSAWithSHA256
ECDSAWithSHA1
ECDSAWithSHA256
ECDSAWithSHA384
ECDSAWithSHA512
SHA256WithRSAPSS
SHA384WithRSAPSS
SHA512WithRSAPSS
)
var SignatureAlgorithmDetails = []struct {
algo SignatureAlgorithm
coseAlg COSEAlgorithmIdentifier
name string
hasher func() hash.Hash
}{
{SHA1WithRSA, AlgRS1, "SHA1-RSA", crypto.SHA1.New},
{SHA256WithRSA, AlgRS256, "SHA256-RSA", crypto.SHA256.New},
{SHA384WithRSA, AlgRS384, "SHA384-RSA", crypto.SHA384.New},
{SHA512WithRSA, AlgRS512, "SHA512-RSA", crypto.SHA512.New},
{SHA256WithRSAPSS, AlgPS256, "SHA256-RSAPSS", crypto.SHA256.New},
{SHA384WithRSAPSS, AlgPS384, "SHA384-RSAPSS", crypto.SHA384.New},
{SHA512WithRSAPSS, AlgPS512, "SHA512-RSAPSS", crypto.SHA512.New},
{ECDSAWithSHA256, AlgES256, "ECDSA-SHA256", crypto.SHA256.New},
{ECDSAWithSHA384, AlgES384, "ECDSA-SHA384", crypto.SHA384.New},
{ECDSAWithSHA512, AlgES512, "ECDSA-SHA512", crypto.SHA512.New},
{UnknownSignatureAlgorithm, AlgEdDSA, "EdDSA", crypto.SHA512.New},
}
type Error struct {
// Short name for the type of error that has occurred
Type string `json:"type"`
// Additional details about the error
Details string `json:"error"`
// Information to help debug the error
DevInfo string `json:"debug"`
}
var (
ErrUnsupportedKey = &Error{
Type: "invalid_key_type",
Details: "Unsupported Public Key Type",
}
ErrUnsupportedAlgorithm = &Error{
Type: "unsupported_key_algorithm",
Details: "Unsupported public key algorithm",
}
ErrSigNotProvidedOrInvalid = &Error{
Type: "signature_not_provided_or_invalid",
Details: "Signature invalid or not provided",
}
)
func (err *Error) Error() string {
return err.Details
}
func (passedError *Error) WithDetails(details string) *Error {
err := *passedError
err.Details = details
return &err
}