Add Nix flake for mciasctl and mciasgrpcctl

Vendor dependencies and expose control program binaries via
nix build. Uses nixpkgs-unstable for Go 1.26 support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 21:01:21 -07:00
parent 35e96444aa
commit 115f23a3ea
2485 changed files with 6802335 additions and 0 deletions

View File

@@ -0,0 +1,199 @@
package protocol
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
// The CredentialAssertionResponse is the raw response returned to the Relying Party from an authenticator when we request a
// credential for login/assertion.
type CredentialAssertionResponse struct {
PublicKeyCredential
AssertionResponse AuthenticatorAssertionResponse `json:"response"`
}
// The ParsedCredentialAssertionData is 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"`
}
// ParsedAssertionResponse is the parsed form of [AuthenticatorAssertionResponse].
type ParsedAssertionResponse struct {
CollectedClientData CollectedClientData
AuthenticatorData AuthenticatorData
Signature []byte
UserHandle []byte
}
// ParseCredentialRequestResponse parses 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 a [*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")
}
defer func(request *http.Request) {
_, _ = io.Copy(io.Discard, request.Body)
_ = request.Body.Close()
}(response)
return ParseCredentialRequestResponseBody(response.Body)
}
// ParseCredentialRequestResponseBody parses 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) (par *ParsedCredentialAssertionData, err error) {
var car CredentialAssertionResponse
if err = decodeBody(body, &car); err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Assertion").WithInfo(err.Error()).WithError(err)
}
return car.Parse()
}
// ParseCredentialRequestResponseBytes is an alternative version of [ParseCredentialRequestResponseBody] that just takes
// a byte slice.
func ParseCredentialRequestResponseBytes(data []byte) (par *ParsedCredentialAssertionData, err error) {
var car CredentialAssertionResponse
if err = decodeBytes(data, &car); err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Assertion").WithInfo(err.Error()).WithError(err)
}
return car.Parse()
}
// Parse validates and parses the [CredentialAssertionResponse] into a [ParseCredentialCreationResponseBody]. This receiver
// is unlikely to be expressly guaranteed under the versioning policy. Users looking for this guarantee should see
// [ParseCredentialRequestResponseBody] instead, and this receiver should only be used if that function is inadequate
// for their use case.
func (car CredentialAssertionResponse) Parse() (par *ParsedCredentialAssertionData, err error) {
if car.ID == "" {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID missing")
}
if _, err = base64.RawURLEncoding.DecodeString(car.ID); err != nil {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID not base64url encoded").WithError(err)
}
if car.Type != string(PublicKeyCredentialType) {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with bad type")
}
var attachment AuthenticatorAttachment
switch att := AuthenticatorAttachment(car.AuthenticatorAttachment); att {
case Platform, CrossPlatform:
attachment = att
}
par = &ParsedCredentialAssertionData{
ParsedPublicKeyCredential{
ParsedCredential{car.ID, car.Type}, car.RawID, car.ClientExtensionResults, attachment,
},
ParsedAssertionResponse{
Signature: car.AssertionResponse.Signature,
UserHandle: car.AssertionResponse.UserHandle,
},
car,
}
// 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.
if err = json.Unmarshal(car.AssertionResponse.ClientDataJSON, &par.Response.CollectedClientData); err != nil {
return nil, err
}
if err = par.Response.AuthenticatorData.Unmarshal(car.AssertionResponse.AuthenticatorData); err != nil {
return nil, ErrParsingData.WithDetails("Error unmarshalling auth data").WithError(err)
}
return par, nil
}
// Verify the remaining elements of the assertion data by following the steps outlined in the referenced specification
// documentation. It's important to note that the credentialBytes field is the CBOR representation of the credential.
//
// Specification: §7.2 Verifying an Authentication Assertion (https://www.w3.org/TR/webauthn/#sctn-verifying-assertion)
func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode, appID string, verifyUser bool, verifyUserPresence 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, rpOrigins, rpTopOrigins, rpTopOriginsVerify)
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, verifyUserPresence)
if validError != nil {
return validError
}
// 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[:]...) //nolint:gocritic // This is intentional.
var (
key any
err error
)
// If the Session Data does not contain the appID extension or it wasn't reported as used by the Client/RP then we
// use the standard CTAP2 public key parser.
if appID == "" {
key, err = webauthncose.ParsePublicKey(credentialBytes)
} else {
key, err = webauthncose.ParseFIDOPublicKey(credentialBytes)
}
if err != nil {
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error parsing the assertion public key: %+v", err)).WithError(err)
}
valid, err := webauthncose.VerifySignature(key, sigData, p.Response.Signature)
if !valid || err != nil {
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error validating the assertion signature: %+v", err)).WithError(err)
}
return nil
}

View File

@@ -0,0 +1,234 @@
package protocol
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"github.com/google/uuid"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
// AuthenticatorAttestationResponse is 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.
//
// See: https://www.w3.org/TR/webauthn/#typedefdef-publickeycredentialjson
type AuthenticatorAttestationResponse struct {
// The byte slice of clientDataJSON, which becomes CollectedClientData.
AuthenticatorResponse
Transports []string `json:"transports,omitempty"`
AuthenticatorData URLEncodedBase64 `json:"authenticatorData"`
PublicKey URLEncodedBase64 `json:"publicKey"`
PublicKeyAlgorithm int64 `json:"publicKeyAlgorithm"`
// AttestationObject is 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"`
}
// ParsedAttestationResponse is the parsed version of [AuthenticatorAttestationResponse].
type ParsedAttestationResponse struct {
CollectedClientData CollectedClientData
AttestationObject AttestationObject
Transports []AuthenticatorTransport
}
// AttestationObject is the raw attestationObject.
//
// Authenticators SHOULD also provide some form of attestation, if possible. If an authenticator does, 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 MAY either perform self
// attestation of the credential public key with the corresponding credential private key, or otherwise perform no
// attestation. All this information is returned by authenticators any time a new public key credential is generated, in
// the overall form of an attestation object.
//
// Specification: §6.5. Attestation (https://www.w3.org/TR/webauthn/#sctn-attestation)
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]any `json:"attStmt,omitempty"`
}
type NonCompoundAttestationObject struct {
// The format of the Attestation data.
Format string `json:"fmt"`
// The attestation statement data sent back if attestation is requested.
AttStatement map[string]any `json:"attStmt,omitempty"`
}
type attestationFormatValidationHandler func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (attestationType string, x5cs []any, err error)
var attestationRegistry = make(map[AttestationFormat]attestationFormatValidationHandler)
// RegisterAttestationFormat is a method to register attestation formats with the library. Generally using one of the
// locally registered attestation formats is enough.
func RegisterAttestationFormat(format AttestationFormat, 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() (p *ParsedAttestationResponse, err error) {
p = &ParsedAttestationResponse{}
if err = json.Unmarshal(ccr.ClientDataJSON, &p.CollectedClientData); err != nil {
return nil, ErrParsingData.WithInfo(err.Error()).WithError(err)
}
if err = webauthncbor.Unmarshal(ccr.AttestationObject, &p.AttestationObject); err != nil {
return nil, ErrParsingData.WithInfo(err.Error()).WithError(err)
}
// 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.
if err = p.AttestationObject.AuthData.Unmarshal(p.AttestationObject.RawAuthData); err != nil {
return nil, err
}
if !p.AttestationObject.AuthData.Flags.HasAttestedCredentialData() {
return nil, ErrAttestationFormat.WithInfo("Attestation missing attested credential data flag")
}
for _, t := range ccr.Transports {
if transport, ok := internalRemappedAuthenticatorTransport[t]; ok {
p.Transports = append(p.Transports, transport)
} else {
p.Transports = append(p.Transports, AuthenticatorTransport(t))
}
}
return p, nil
}
// Verify performs Steps 13 through 19 of registration verification.
//
// Steps 13 through 15 are verified against the auth data. These steps are identical to 15 through 18 for assertion so we
// handle them with AuthData.
func (a *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, userVerificationRequired bool, userPresenceRequired bool, mds metadata.Provider, credParams []CredentialParameter) (err error) {
rpIDHash := sha256.Sum256([]byte(relyingPartyID))
// Begin Step 13 through 15. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP.
if err = a.AuthData.Verify(rpIDHash[:], nil, userVerificationRequired, userPresenceRequired); err != nil {
return err
}
// Step 16. Verify that the "alg" parameter in the credential public key in
// authData matches the alg attribute of one of the items in options.pubKeyCredParams.
var pk webauthncose.PublicKeyData
if err = webauthncbor.Unmarshal(a.AuthData.AttData.CredentialPublicKey, &pk); err != nil {
return err
}
found := false
for _, credParam := range credParams {
if int(pk.Algorithm) == int(credParam.Algorithm) {
found = true
break
}
}
if !found {
return ErrAttestationFormat.WithInfo("Credential public key algorithm not supported")
}
return a.VerifyAttestation(clientDataHash, mds)
}
// VerifyAttestation only verifies the attestation object excluding the AuthData values. If you wish to also verify the
// AuthData values you should use [Verify].
func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadata.Provider) (err error) {
// Step 18. 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 AttestationFormat(a.Format) == AttestationFormatNone {
if len(a.AttStatement) != 0 {
return ErrAttestationFormat.WithInfo("Attestation format none with attestation present")
}
return nil
}
var (
handler attestationFormatValidationHandler
valid bool
)
if handler, valid = attestationRegistry[AttestationFormat(a.Format)]; !valid {
return ErrAttestationFormat.WithInfo(fmt.Sprintf("Attestation format %s is unsupported", a.Format))
}
var (
aaguid uuid.UUID
attestationType string
x5cs []any
)
// Step 19. 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.
if attestationType, x5cs, err = handler(*a, clientDataHash, mds); err != nil {
return err.(*Error).WithInfo(attestationType)
}
if attestationType == string(AttestationFormatCompound) {
return nil
}
if len(a.AuthData.AttData.AAGUID) != 0 {
if aaguid, err = uuid.FromBytes(a.AuthData.AttData.AAGUID); err != nil {
return ErrInvalidAttestation.WithInfo("Error occurred parsing AAGUID during attestation validation").WithDetails(err.Error()).WithError(err)
}
}
if mds == nil {
return nil
}
if e := ValidateMetadata(context.Background(), mds, aaguid, attestationType, a.Format, x5cs); e != nil {
return ErrInvalidAttestation.WithInfo(fmt.Sprintf("Error occurred validating metadata during attestation validation: %+v", e)).WithDetails(e.DevInfo).WithError(e)
}
return nil
}

View File

@@ -0,0 +1,260 @@
package protocol
import (
"bytes"
"crypto/x509"
"encoding/asn1"
"fmt"
"time"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
// attestationFormatValidationHandlerAndroidKey is the handler for the Android Key Attestation Statement Format.
//
// An Android key attestation statement consists simply of the Android attestation statement, which is a series of DER
// encoded X.509 certificates. See the Android developer documentation. Its syntax is defined as follows:
//
// $$attStmtType //= (
//
// fmt: "android-key",
// attStmt: androidStmtFormat
// )
//
// androidStmtFormat = {
// alg: COSEAlgorithmIdentifier,
// sig: bytes,
// x5c: [ credCert: bytes, * (caCert: bytes) ]
// }
//
// Specification: §8.4. Android Key Attestation Statement Format
//
// See: https://www.w3.org/TR/webauthn/#sctn-android-key-attestation
func attestationFormatValidationHandlerAndroidKey(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
var (
alg int64
sig []byte
ok bool
)
// 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.
if alg, ok = att.AttStatement[stmtAlgorithm].(int64); !ok {
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving alg value")
}
// Get the sig value - A byte string containing the attestation signature.
if sig, ok = att.AttStatement[stmtSignature].([]byte); !ok {
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving sig 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.
var (
x5c []any
certs []*x509.Certificate
)
if x5c, certs, err = attStatementParseX5CS(att.AttStatement, stmtX5C); err != nil {
return "", nil, err
}
if len(certs) == 0 {
return "", nil, ErrInvalidAttestation.WithDetails("No certificates in x5c")
}
credCert := certs[0]
if _, err = attStatementCertChainVerify(certs, attAndroidKeyHardwareRootsCertPool, true, time.Now().Add(time.Hour*8760).UTC()); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails("Error validating x5c cert chain").WithError(err)
}
signatureData := append(att.RawAuthData, clientDataHash...) //nolint:gocritic // This is intentional.
if sigAlg := webauthncose.SigAlgFromCOSEAlg(webauthncose.COSEAlgorithmIdentifier(alg)); sigAlg == x509.UnknownSignatureAlgorithm {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Unsupported COSE alg: %d", alg))
} else if err = credCert.CheckSignature(sigAlg, signatureData, sig); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v", err)).WithError(err)
}
// Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData.
var attPublicKeyData webauthncose.EC2PublicKeyData
if attPublicKeyData, err = verifyAttestationECDSAPublicKeyMatch(att, credCert); err != nil {
return "", nil, err
}
var valid bool
if valid, err = attPublicKeyData.Verify(signatureData, sig); err != nil || !valid {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v", err)).WithError(err)
}
// §8.4.3. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
// attCert.Extensions.
// As noted in §8.4.1 (https://www.w3.org/TR/webauthn/#key-attstn-cert-requirements) the Android Key Attestation
// certificate's android key attestation certificate extension data is identified by the OID
// "1.3.6.1.4.1.11129.2.1.17".
var attExtBytes []byte
for _, ext := range credCert.Extensions {
if ext.Id.Equal(oidExtensionAndroidKeystore) {
attExtBytes = ext.Value
}
}
if len(attExtBytes) == 0 {
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.3.6.1.4.1.11129.2.1.17")
}
decoded := keyDescription{}
if _, err = asn1.Unmarshal(attExtBytes, &decoded); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Unable to parse Android key attestation certificate extensions").WithError(err)
}
// Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
if !bytes.Equal(decoded.AttestationChallenge, clientDataHash) {
return "", 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 decoded.SoftwareEnforced.AllApplications != nil || decoded.TeeEnforced.AllApplications != nil {
return "", 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 decoded.SoftwareEnforced.Origin != KM_ORIGIN_GENERATED || decoded.TeeEnforced.Origin != KM_ORIGIN_GENERATED {
return "", 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 "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with purpose not equal KM_PURPOSE_SIGN")
}
return string(metadata.BasicFull), 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 any `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 any `asn1:"tag:503,explicit,optional"`
UserAuthType int `asn1:"tag:504,explicit,optional"`
AuthTimeout int `asn1:"tag:505,explicit,optional"`
AllowWhileOnBody any `asn1:"tag:506,explicit,optional"`
TrustedUserPresenceRequired any `asn1:"tag:507,explicit,optional"`
TrustedConfirmationRequired any `asn1:"tag:508,explicit,optional"`
UnlockedDeviceRequired any `asn1:"tag:509,explicit,optional"`
AllApplications any `asn1:"tag:600,explicit,optional"`
ApplicationID any `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 //nolint:unused
deviceLocked bool //nolint:unused
verifiedBootState verifiedBootState //nolint:unused
verifiedBootHash []byte //nolint:unused
}
type verifiedBootState int
const (
Verified verifiedBootState = iota
SelfSigned
Unverified
Failed
)
const (
// KM_ORIGIN_GENERATED means generated in keymaster. Should not exist outside the TEE.
KM_ORIGIN_GENERATED = iota
// KM_ORIGIN_DERIVED means derived inside keymaster. Likely exists off-device.
KM_ORIGIN_DERIVED
// KM_ORIGIN_IMPORTED means imported into keymaster. Existed as clear text in Android.
KM_ORIGIN_IMPORTED
// KM_ORIGIN_UNKNOWN means 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 unknown whether the key
// was generated inside or imported into keymaster.
KM_ORIGIN_UNKNOWN
)
const (
// KM_PURPOSE_ENCRYPT is usable with RSA, EC and AES keys.
KM_PURPOSE_ENCRYPT = iota
// KM_PURPOSE_DECRYPT is usable with RSA, EC and AES keys.
KM_PURPOSE_DECRYPT
// KM_PURPOSE_SIGN is usable with RSA, EC and HMAC keys.
KM_PURPOSE_SIGN
// KM_PURPOSE_VERIFY is usable with RSA, EC and HMAC keys.
KM_PURPOSE_VERIFY
// KM_PURPOSE_DERIVE_KEY is usable with EC keys.
KM_PURPOSE_DERIVE_KEY
// KM_PURPOSE_WRAP is usable with wrapped keys.
KM_PURPOSE_WRAP
)
var (
attAndroidKeyHardwareRootsCertPool *x509.CertPool
)
func init() {
RegisterAttestationFormat(AttestationFormatAndroidKey, attestationFormatValidationHandlerAndroidKey)
}

View File

@@ -0,0 +1,99 @@
package protocol
import (
"bytes"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"time"
"github.com/go-webauthn/webauthn/metadata"
)
// attestationFormatValidationHandlerAppleAnonymous is the handler for the Apple Anonymous Attestation Statement Format.
//
// The syntax of an Apple attestation statement is defined as follows:
//
// $$attStmtType //= (
//
// fmt: "apple",
// attStmt: appleStmtFormat
// )
//
// appleStmtFormat = {
// x5c: [ credCert: bytes, * (caCert: bytes) ]
// }
//
// Specification: §8.8. Apple Anonymous Attestation Statement Format
//
// See : https://www.w3.org/TR/webauthn/#sctn-apple-anonymous-attestation
func attestationFormatValidationHandlerAppleAnonymous(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (attestationType string, x5cs []any, err 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.
var (
x5c []any
certs []*x509.Certificate
)
if x5c, certs, err = attStatementParseX5CS(att.AttStatement, stmtX5C); err != nil {
return "", nil, err
}
credCert := certs[0]
if _, err = attStatementCertChainVerify(certs, attAppleHardwareRootsCertPool, true, time.Now().Add(time.Hour*8760).UTC()); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails("Error validating x5c cert chain").WithError(err)
}
// Step 2. Concatenate authenticatorData and clientDataHash to form nonceToHash.
nonceToHash := append(att.RawAuthData, clientDataHash...) //nolint:gocritic // This is intentional.
// 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(oidExtensionAppleAnonymousAttestation) {
attExtBytes = ext.Value
}
}
if len(attExtBytes) == 0 {
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.2.840.113635.100.8.2")
}
decoded := AppleAnonymousAttestation{}
if _, err = asn1.Unmarshal(attExtBytes, &decoded); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Unable to parse apple attestation certificate extensions").WithError(err)
}
if !bytes.Equal(decoded.Nonce, nonce[:]) {
return "", 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.
if _, err = verifyAttestationECDSAPublicKeyMatch(att, credCert); err != nil {
return "", nil, err
}
// Step 6. If successful, return implementation-specific values representing attestation type Anonymization CA and
// attestation trust path x5c.
return string(metadata.AnonCA), x5c, nil
}
// AppleAnonymousAttestation represents the attestation format for Apple, who have not yet published a schema for the
// extension (as of JULY 2021.)
type AppleAnonymousAttestation struct {
Nonce []byte `asn1:"tag:1,explicit"`
}
var (
attAppleHardwareRootsCertPool *x509.CertPool
)
func init() {
RegisterAttestationFormat(AttestationFormatApple, attestationFormatValidationHandlerAppleAnonymous)
}

View File

@@ -0,0 +1,111 @@
package protocol
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/go-webauthn/webauthn/metadata"
)
func init() {
RegisterAttestationFormat(AttestationFormatCompound, attestationFormatValidationHandlerCompound)
}
// attestationFormatValidationHandlerCompound is the handler for the Compound Attestation Statement Format.
//
// The syntax of a Compound Attestation statement is defined by the following CDDL:
//
// $$attStmtType //= (
//
// fmt: "compound",
// attStmt: [2* nonCompoundAttStmt]
// )
//
// nonCompoundAttStmt = { $$attStmtType } .within { fmt: text .ne "compound", * any => any }
//
// Specification: §8.9. Compound Attestation Statement Forma
//
// See: https://www.w3.org/TR/webauthn-3/#sctn-compound-attestation
func attestationFormatValidationHandlerCompound(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (attestationType string, x5cs []any, err error) {
var (
aaguid uuid.UUID
raw any
ok bool
stmts []any
subStmt map[string]any
attStmts []NonCompoundAttestationObject
)
if len(att.AuthData.AttData.AAGUID) != 0 {
if aaguid, err = uuid.FromBytes(att.AuthData.AttData.AAGUID); err != nil {
return "", nil, ErrInvalidAttestation.WithInfo("Error occurred parsing AAGUID during attestation validation").WithDetails(err.Error()).WithError(err)
}
}
if raw, ok = att.AttStatement[stmtAttStmt]; !ok {
return "", nil, ErrInvalidAttestation.WithDetails("Compound statement missing attStmt")
}
if stmts, ok = raw.([]any); !ok {
return "", nil, ErrInvalidAttestation.WithDetails("Compound statement attStmt isn't an array")
}
if len(stmts) < 2 {
return "", nil, ErrInvalidAttestation.WithDetails("Compound statement attStmt isn't an array with at least two other statements")
}
for _, stmt := range stmts {
if subStmt, ok = stmt.(map[string]any); !ok {
return "", nil, ErrInvalidAttestation.WithDetails("Compound statement attStmt contains one or more items that isn't an object")
}
var attStmt NonCompoundAttestationObject
if attStmt.Format, ok = subStmt[stmtFmt].(string); !ok {
return "", nil, ErrInvalidAttestation.WithDetails("Compound sub-statement does not have a format")
}
if attStmt.AttStatement, ok = subStmt[stmtAttStmt].(map[string]any); !ok {
return "", nil, ErrInvalidAttestation.WithDetails("Compound sub-statement does not have an attestation statement")
}
switch AttestationFormat(attStmt.Format) {
case AttestationFormatCompound:
return "", nil, ErrInvalidAttestation.WithDetails("Compound sub-statement has a format of compound which is not allowed")
case "":
return "", nil, ErrInvalidAttestation.WithDetails("Compound sub-statement has an empty format which is not allowed")
default:
if _, ok = attestationRegistry[AttestationFormat(attStmt.Format)]; !ok {
return "", nil, ErrAttestationFormat.WithInfo(fmt.Sprintf("Attestation sub-statement format %s is unsupported", attStmt.Format))
}
attStmts = append(attStmts, attStmt)
}
}
for _, attStmt := range attStmts {
object := AttestationObject{
Format: attStmt.Format,
AttStatement: attStmt.AttStatement,
AuthData: att.AuthData,
RawAuthData: att.RawAuthData,
}
var cx5cs []any
if _, cx5cs, err = attestationRegistry[AttestationFormat(object.Format)](object, clientDataHash, mds); err != nil {
return "", nil, err
}
if mds == nil {
continue
}
if e := ValidateMetadata(context.Background(), mds, aaguid, attestationType, object.Format, cx5cs); e != nil {
return "", nil, ErrInvalidAttestation.WithInfo(fmt.Sprintf("Error occurred validating metadata during attestation validation: %+v", e)).WithDetails(e.DevInfo).WithError(e)
}
}
return string(AttestationFormatCompound), nil, nil
}

View File

@@ -0,0 +1,159 @@
package protocol
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/x509"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
// attestationFormatValidationHandlerFIDOU2F is the handler for the FIDO U2F Attestation Statement Format.
//
// The syntax of a FIDO U2F attestation statement is defined as follows:
//
// $$attStmtType //= (
//
// fmt: "fido-u2f",
// attStmt: u2fStmtFormat
// )
//
// u2fStmtFormat = {
// x5c: [ attestnCert: bytes ],
// sig: bytes
// }
//
// Specification: §8.6. FIDO U2F Attestation Statement Format
//
// See: https://www.w3.org/TR/webauthn/#sctn-fido-u2f-attestation
func attestationFormatValidationHandlerFIDOU2F(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
// Non-normative verification procedure of expected requirement.
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 "", nil, ErrUnsupportedAlgorithm.WithDetails("U2F attestation format AAGUID not set to 0x00")
}
// Signing procedure. Non-normative verification procedure of expected requirement.
// If the credential public key of the attested credential is not of algorithm -7 ("ES256"), stop and return an error.
var key webauthncose.EC2PublicKeyData
if err = webauthncbor.Unmarshal(att.AuthData.AttData.CredentialPublicKey, &key); err != nil {
return "", nil, ErrAttestationCertificate.WithDetails("Error parsing public key").WithError(err)
}
if webauthncose.COSEAlgorithmIdentifier(key.Algorithm) != webauthncose.AlgES256 {
return "", nil, ErrUnsupportedAlgorithm.WithDetails("Non-ES256 Public Key algorithm used")
}
var (
sig []byte
raw []byte
x5c []any
ok bool
)
// 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.
// Check for "x5c" which is a single element array containing the attestation certificate in X.509 format.
if x5c, ok = att.AttStatement[stmtX5C].([]any); !ok {
return "", nil, ErrAttestationFormat.WithDetails("Missing properly formatted x5c data")
}
// Note: Packed Attestation, FIDO U2F Attestation, and Assertion Signatures require ASN.1 DER sig values, 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 [RFC9053](https://www.rfc-editor.org/rfc/rfc9053.html) and
// [RFC8230](https://www.rfc-editor.org/rfc/rfc8230.html).
// This is described in §6.5.5 https://www.w3.org/TR/webauthn-3/#sctn-signature-attestation-types.
// 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.
if sig, ok = att.AttStatement[stmtSignature].([]byte); !ok {
return "", nil, ErrAttestationFormat.WithDetails("Missing sig data")
}
// 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 "", nil, ErrAttestationFormat.WithDetails("Received more than one element in x5c values")
}
// Step 2.2.
if raw, ok = x5c[0].([]byte); !ok {
return "", nil, ErrAttestationFormat.WithDetails("Error decoding ASN.1 data from x5c")
}
attCert, err := x509.ParseCertificate(raw)
if err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1 data into certificate").WithError(err)
}
// Step 2.3.
if attCert.PublicKeyAlgorithm != x509.ECDSA {
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate public key algorithm is not ECDSA")
}
// 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
// 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://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats)).
// 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.
credentialPublicKey, ok := attCert.PublicKey.(*ecdsa.PublicKey)
if !ok || credentialPublicKey.Curve != elliptic.P256() {
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate does not contain a P-256 ECDSA public key")
}
if len(key.XCoord) > 32 || len(key.YCoord) > 32 {
return "", 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 Section 4.3 of [FIDO-U2F-Message-Formats](https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html#registration-response-message-success)).
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 the certificate public key per section 4.1.4 of [SEC1] with
// SHA-256 as the hash function used in step two.
if err = attCert.CheckSignature(x509.ECDSAWithSHA256, verificationData.Bytes(), sig); err != nil {
return "", nil, err
}
// TODO: Step 7. Optionally, inspect x5c and consult externally provided knowledge to determine whether attStmt
// conveys a Basic or AttCA attestation.
// Step 8. If successful, return implementation-specific values representing attestation type Basic, AttCA or
// uncertainty, and attestation trust path x5c.
return string(metadata.BasicFull), x5c, nil
}
func init() {
RegisterAttestationFormat(AttestationFormatFIDOUniversalSecondFactor, attestationFormatValidationHandlerFIDOU2F)
}

View File

@@ -0,0 +1,255 @@
package protocol
import (
"bytes"
"crypto/x509"
"encoding/asn1"
"fmt"
"strings"
"time"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
func init() {
RegisterAttestationFormat(AttestationFormatPacked, attestationFormatValidationHandlerPacked)
}
// attestationFormatValidationHandlerPacked is the handler for the Packed Attestation Statement Format.
//
// The syntax of a Packed Attestation statement is defined by the following CDDL:
//
// $$attStmtType //= (
//
// fmt: "packed",
// attStmt: packedStmtFormat
// )
//
// packedStmtFormat = {
// alg: COSEAlgorithmIdentifier,
// sig: bytes,
// x5c: [ attestnCert: bytes, * (caCert: bytes) ]
// } //
// {
// alg: COSEAlgorithmIdentifier
// sig: bytes,
// }
//
// Specification: §8.2. Packed Attestation Statement Format
//
// See: https://www.w3.org/TR/webauthn/#sctn-packed-attestation
func attestationFormatValidationHandlerPacked(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (attestationType string, x5cs []any, err error) {
var (
alg int64
sig []byte
x5c []any
ok bool
)
// 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.
if alg, ok = att.AttStatement[stmtAlgorithm].(int64); !ok {
return string(AttestationFormatPacked), nil, ErrAttestationFormat.WithDetails("Error retrieving alg value")
}
// Get the sig value - A byte string containing the attestation signature.
if sig, ok = att.AttStatement[stmtSignature].([]byte); !ok {
return string(AttestationFormatPacked), nil, ErrAttestationFormat.WithDetails("Error retrieving sig value")
}
// Step 2. If x5c is present, this indicates that the attestation type is not ECDAA.
if x5c, ok = att.AttStatement[stmtX5C].([]any); ok {
// Handle Basic Attestation steps for the x509 Certificate.
return handleBasicAttestation(sig, clientDataHash, att.RawAuthData, att.AuthData.AttData.AAGUID, alg, x5c, mds)
}
// Step 3. If ecdaaKeyId is present, then the attestation type is ECDAA.
// Also make sure the we did not have an x509.
ecdaaKeyID, ecdaaKeyPresent := att.AttStatement[stmtECDAAKID].([]byte)
if ecdaaKeyPresent {
// Handle ECDAA Attestation steps for the x509 Certificate.
return handleECDAAAttestation(sig, clientDataHash, ecdaaKeyID, mds)
}
// 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, mds)
}
// Handle the attestation steps laid out in the basic format.
func handleBasicAttestation(sig, clientDataHash, authData, aaguid []byte, alg int64, x5c []any, _ metadata.Provider) (attestationType string, x5cs []any, err 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.
var attestnCert *x509.Certificate
for i, raw := range x5c {
rawByes, ok := raw.([]byte)
if !ok {
return "", x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
cert, err := x509.ParseCertificate(rawByes)
if err != nil {
return "", x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err)).WithError(err)
}
if cert.NotBefore.After(time.Now()) || cert.NotAfter.Before(time.Now()) {
return "", x5c, ErrAttestationFormat.WithDetails("Cert in chain not time valid")
}
if i == 0 {
attestnCert = cert
}
}
if attestnCert == nil {
return "", x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
signatureData := append(authData, clientDataHash...) //nolint:gocritic // This is intentional.
if sigAlg := webauthncose.SigAlgFromCOSEAlg(webauthncose.COSEAlgorithmIdentifier(alg)); sigAlg == x509.UnknownSignatureAlgorithm {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Unsupported COSE alg: %d", alg))
} else if err = attestnCert.CheckSignature(sigAlg, signatureData, sig); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v", err)).WithError(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 attestnCert.Version != 3 {
return "", 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, usable, country code library. For now, check stringy-ness
subjectString := strings.Join(attestnCert.Subject.Country, "")
if subjectString == "" {
return "", x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Country Code is invalid")
}
// Subject-O
// Legal name of the Authenticator vendor (UTF8String).
subjectString = strings.Join(attestnCert.Subject.Organization, "")
if subjectString == "" {
return "", x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Organization is invalid")
}
// Subject-OU
// Literal string “Authenticator Attestation” (UTF8String).
subjectString = strings.Join(attestnCert.Subject.OrganizationalUnit, " ")
if subjectString != "Authenticator Attestation" {
return "", x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Organizational Unit is invalid")
}
// Subject-CN
// A UTF8String of the vendors choosing.
subjectString = attestnCert.Subject.CommonName
if subjectString == "" {
return "", x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Common Name not set")
}
// 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.
var foundAAGUID []byte
for _, extension := range attestnCert.Extensions {
if extension.Id.Equal(oidFIDOGenCeAAGUID) {
if extension.Critical {
return "", 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 {
var unMarshalledAAGUID []byte
if _, err = asn1.Unmarshal(foundAAGUID, &unMarshalledAAGUID); err != nil {
return "", x5c, ErrInvalidAttestation.WithDetails("Error unmarshalling AAGUID from certificate")
}
if !bytes.Equal(aaguid, unMarshalledAAGUID) {
return "", x5c, ErrInvalidAttestation.WithDetails("Certificate AAGUID does not match Auth Data certificate")
}
}
// Step 2.2.4 The Basic Constraints extension MUST have the CA component set to false.
if attestnCert.IsCA {
return "", 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 string(metadata.BasicFull), x5c, nil
}
func handleECDAAAttestation(sig, clientDataHash, ecdaaKeyID []byte, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
return "Packed (ECDAA)", nil, ErrNotSpecImplemented
}
func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, sig []byte, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
verificationData := append(authData, clientDataHash...) //nolint:gocritic // This is intentional.
var (
key any
valid bool
)
if key, err = webauthncose.ParsePublicKey(pubKey); err != nil {
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the public key: %+v", err))
}
// §4.1 Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData.
switch k := key.(type) {
case webauthncose.OKPPublicKeyData:
err = verifyKeyAlgorithm(k.Algorithm, alg)
case webauthncose.EC2PublicKeyData:
err = verifyKeyAlgorithm(k.Algorithm, alg)
case webauthncose.RSAPublicKeyData:
err = verifyKeyAlgorithm(k.Algorithm, alg)
default:
return "", nil, ErrInvalidAttestation.WithDetails("Error verifying the public key data")
}
if err != nil {
return "", nil, err
}
// §4.2 Verify that sig is a valid signature over the concatenation of authenticatorData and
// clientDataHash using the credential public key with alg.
if valid, err = webauthncose.VerifySignature(key, verificationData, sig); err != nil {
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error verifying the signature: %+v", err)).WithError(err)
} else if !valid {
return "", nil, ErrInvalidAttestation.WithDetails("Unable to verify signature")
}
return string(metadata.BasicSurrogate), 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
}

View File

@@ -0,0 +1,185 @@
package protocol
import (
"bytes"
"context"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"fmt"
"time"
"github.com/go-viper/mapstructure/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/go-webauthn/webauthn/metadata"
)
// attestationFormatValidationHandlerAndroidSafetyNet is the handler for the Android SafetyNet Attestation Statement
// Format.
//
// When the authenticator is a platform authenticator on certain Android platforms, the attestation statement may be
// 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 provides some
// statements about the health of the platform and the identity of the calling application (see SafetyNet Documentation
// for more details).
//
// The syntax of an Android Attestation statement is defined as follows:
//
// $$attStmtType //= (
// fmt: "android-safetynet",
// attStmt: safetynetStmtFormat
// )
//
// safetynetStmtFormat = {
// ver: text,
// response: bytes
// }
//
// Specification: §8.5. Android SafetyNet Attestation Statement Format
//
// See: https://www.w3.org/TR/webauthn/#sctn-android-safetynet-attestation
func attestationFormatValidationHandlerAndroidSafetyNet(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (attestationType string, x5cs []any, err 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[stmtVersion].(string)
if !present {
return "", nil, ErrAttestationFormat.WithDetails("Unable to find the version of SafetyNet")
}
if version == "" {
return "", 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 "", nil, ErrAttestationFormat.WithDetails("Unable to find the SafetyNet response")
}
var token *jwt.Token
if token, err = jwt.Parse(string(response), keyFuncSafetyNetJWT, jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()})); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
}
// marshall the JWT payload into the safetynet response json.
var safetyNetResponse SafetyNetResponse
if err = mapstructure.Decode(token.Claims, &safetyNetResponse); err != nil {
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the SafetyNet response: %+v", err)).WithError(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 "", nil, ErrInvalidAttestation.WithDetails("Invalid nonce for in SafetyNet response").WithError(err)
}
// §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate)
certChain := token.Header[stmtX5C].([]any)
l := make([]byte, base64.StdEncoding.DecodedLen(len(certChain[0].(string))))
n, err := base64.StdEncoding.Decode(l, []byte(certChain[0].(string)))
if err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
}
attestationCert, err := x509.ParseCertificate(l[:n])
if err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
}
// §8.5.5 Verify that attestationCert is issued to the hostname "attest.android.com".
if err = attestationCert.VerifyHostname(attStatementAndroidSafetyNetHostname); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
}
// §8.5.6 Verify that the ctsProfileMatch attribute in the payload of response is true.
if !safetyNetResponse.CtsProfileMatch {
return "", nil, ErrInvalidAttestation.WithDetails("ctsProfileMatch attribute of the JWT payload is false")
}
if t := time.Unix(safetyNetResponse.TimestampMs/1000, 0); t.After(time.Now()) {
// Zero tolerance for post-dated timestamps.
return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time")
} else if t.Before(time.Now().Add(-time.Minute)) {
// Small tolerance for pre-dated timestamps.
if mds != nil && mds.GetValidateEntry(context.Background()) {
return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp before one minute ago")
}
}
// §8.5.7 If successful, return implementation-specific values representing attestation type Basic and attestation
// trust path attestationCert.
return string(metadata.BasicFull), nil, nil
}
func keyFuncSafetyNetJWT(token *jwt.Token) (key any, err error) {
var (
ok bool
raw any
chain []any
first string
der []byte
cert *x509.Certificate
)
if raw, ok = token.Header[stmtX5C]; !ok {
return nil, fmt.Errorf("jwt header missing x5c")
}
if chain, ok = raw.([]any); !ok || len(chain) == 0 {
return nil, fmt.Errorf("jwt header x5c is not a non-empty array")
}
if first, ok = chain[0].(string); !ok || first == "" {
return nil, fmt.Errorf("jwt header x5c[0] not a base64 string")
}
if der, err = base64.StdEncoding.DecodeString(first); err != nil {
return nil, fmt.Errorf("decode x5c leaf: %w", err)
}
if cert, err = x509.ParseCertificate(der); err != nil {
if cert != nil {
return cert.PublicKey, fmt.Errorf("parse x5c leaf: %w", err)
}
return nil, fmt.Errorf("parse x5c leaf: %w", err)
}
return cert.PublicKey, nil
}
type SafetyNetResponse struct {
Nonce string `json:"nonce"`
TimestampMs int64 `json:"timestampMs"`
ApkPackageName string `json:"apkPackageName"`
ApkDigestSha256 string `json:"apkDigestSha256"`
CtsProfileMatch bool `json:"ctsProfileMatch"`
ApkCertificateDigestSha256 []any `json:"apkCertificateDigestSha256"`
BasicIntegrity bool `json:"basicIntegrity"`
}
func init() {
RegisterAttestationFormat(AttestationFormatAndroidSafetyNet, attestationFormatValidationHandlerAndroidSafetyNet)
}

View File

@@ -0,0 +1,625 @@
package protocol
import (
"bytes"
"crypto"
"crypto/subtle"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/binary"
"errors"
"fmt"
"strings"
"github.com/google/go-tpm/tpm2"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
// attestationFormatValidationHandlerTPM is the handler for the TPM Attestation Statement Format.
//
// The syntax of a TPM Attestation statement is as follows:
//
// $$attStmtType // = (
//
// fmt: "tpm",
// attStmt: tpmStmtFormat
// )
//
// tpmStmtFormat = {
// ver: "2.0",
// (
// alg: COSEAlgorithmIdentifier,
// x5c: [ aikCert: bytes, * (caCert: bytes) ]
// )
// sig: bytes,
// certInfo: bytes,
// pubArea: bytes
// }
//
// Specification: §8.3. TPM Attestation Statement Format
//
// See: https://www.w3.org/TR/webauthn/#sctn-tpm-attestation
func attestationFormatValidationHandlerTPM(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
var statement *tpm2AttStatement
if statement, err = newTPM2AttStatement(att.AttStatement); err != nil {
return "", nil, err
}
if statement.HasECDAAKeyID || statement.HasValidECDAAKeyID {
return "", nil, ErrNotImplemented
}
if !statement.HasX5C || !statement.HasValidX5C {
return "", nil, ErrNotImplemented
}
if statement.Version != versionTPM20 {
return "", nil, ErrAttestationFormat.WithDetails("WebAuthn only supports TPM 2.0 currently")
}
var (
pubArea *tpm2.TPMTPublic
key any
)
if pubArea, err = tpm2.Unmarshal[tpm2.TPMTPublic](statement.PubArea); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Unable to decode TPMT_PUBLIC in attestation statement").WithError(err)
}
if key, err = webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey); err != nil {
return "", nil, err
}
switch k := key.(type) {
case webauthncose.EC2PublicKeyData:
var (
params *tpm2.TPMSECCParms
point *tpm2.TPMSECCPoint
)
if params, err = pubArea.Parameters.ECCDetail(); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between ECCParameters in pubArea and credentialPublicKey")
}
if point, err = pubArea.Unique.ECC(); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between ECCParameters in pubArea and credentialPublicKey")
}
if params.CurveID != k.TPMCurveID() {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between ECCParameters in pubArea and credentialPublicKey")
}
if !bytes.Equal(point.X.Buffer, k.XCoord) || !bytes.Equal(point.Y.Buffer, k.YCoord) {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between ECCParameters in pubArea and credentialPublicKey")
}
case webauthncose.RSAPublicKeyData:
var (
params *tpm2.TPMSRSAParms
modulus *tpm2.TPM2BPublicKeyRSA
)
if params, err = pubArea.Parameters.RSADetail(); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey")
}
if modulus, err = pubArea.Unique.RSA(); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey")
}
if !bytes.Equal(modulus.Buffer, k.Modulus) {
return "", nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey")
}
exp := uint32(k.Exponent[0]) + uint32(k.Exponent[1])<<8 + uint32(k.Exponent[2])<<16
if tpm2Exponent(params) != exp {
return "", 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...) //nolint:gocritic // This is intentional.
var certInfo *tpm2.TPMSAttest
// Validate that certInfo is valid:
// 1/4 Verify that magic is set to TPM_GENERATED_VALUE, handled here.
if certInfo, err = tpm2.Unmarshal[tpm2.TPMSAttest](statement.CertInfo); err != nil {
return "", nil, err
}
if err = certInfo.Magic.Check(); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails("Magic is not set to TPM_GENERATED_VALUE")
}
// 2/4 Verify that type is set to TPM_ST_ATTEST_CERTIFY.
if certInfo.Type != tpm2.TPMSTAttestCertify {
return "", 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".
coseAlg := webauthncose.COSEAlgorithmIdentifier(statement.Algorithm)
h := webauthncose.HasherFromCOSEAlg(coseAlg)
h.Write(attToBeSigned)
if !bytes.Equal(certInfo.ExtraData.Buffer, h.Sum(nil)) {
return "", nil, ErrAttestationFormat.WithDetails("ExtraData is not set to hash of attToBeSigned")
}
// 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.
var (
aikCert *x509.Certificate
raw []byte
ok bool
)
if len(statement.X5C) == 0 {
return "", nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
// 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.
if raw, ok = statement.X5C[0].([]byte); !ok {
return "", nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
if aikCert, err = x509.ParseCertificate(raw); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1")
}
if sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg); sigAlg == x509.UnknownSignatureAlgorithm {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Unsupported COSE alg: %d", statement.Algorithm))
} else if err = aikCert.CheckSignature(sigAlg, statement.CertInfo, statement.Signature); err != nil {
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Signature validation error: %+v", 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 "", nil, ErrAttestationFormat.WithDetails("AIK certificate version must be 3")
}
// 2/6 Subject field MUST be set to empty.
if aikCert.Subject.String() != "" {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate subject must be empty")
}
var (
manufacturer, model, version string
ekuValid = false
eku []asn1.ObjectIdentifier
constraints tpmBasicConstraints
rest []byte
)
for _, ext := range aikCert.Extensions {
switch {
case ext.Id.Equal(oidExtensionSubjectAltName):
if manufacturer, model, version, err = parseSANExtension(ext.Value); err != nil {
return "", nil, err
}
case ext.Id.Equal(oidExtensionExtendedKeyUsage):
if rest, err = asn1.Unmarshal(ext.Value, &eku); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate extended key usage malformed")
} else if len(rest) != 0 {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate extended key usage contains extra data")
}
found := false
for _, oid := range eku {
if oid.Equal(oidTCGKpAIKCertificate) {
found = true
break
}
}
if !found {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate extended key usage missing 2.23.133.8.3")
}
ekuValid = true
case ext.Id.Equal(oidExtensionBasicConstraints):
if rest, err = asn1.Unmarshal(ext.Value, &constraints); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints malformed")
} else if len(rest) != 0 {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints contains extra data")
}
}
}
// 3/6 The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9.
if manufacturer == "" || model == "" || version == "" {
return "", nil, ErrAttestationFormat.WithDetails("Invalid SAN data in AIK certificate")
}
if !isValidTPMManufacturer(manufacturer) {
return "", 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.
if !ekuValid {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate missing EKU")
}
// 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.
if constraints.IsCA {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints missing or CA is true")
}
// 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.
//
// This needs to move after the x5c check as the QualifiedSigner only gets populated when it can be verified.
if ok, err = tpm2NameMatch(certInfo, pubArea); err != nil {
return "", nil, err
} else if !ok {
return "", nil, ErrAttestationFormat.WithDetails("Hash value mismatch attested and pubArea")
}
return string(metadata.AttCA), statement.X5C, err
}
func tpm2Exponent(params *tpm2.TPMSRSAParms) (exp uint32) {
if params.Exponent != 0 {
return params.Exponent
}
return 65537
}
func tpm2NameMatch(certInfo *tpm2.TPMSAttest, pubArea *tpm2.TPMTPublic) (match bool, err error) {
if certInfo == nil || pubArea == nil {
return false, nil
}
var (
certifyInfo *tpm2.TPMSCertifyInfo
name *tpm2.TPM2BName
)
if certifyInfo, err = certInfo.Attested.Certify(); err != nil {
return false, err
}
if name, err = tpm2.ObjectName(pubArea); err != nil {
return false, err
}
if _, _, err = tpm2NameDigest(certInfo.QualifiedSigner); err != nil {
return false, fmt.Errorf("invalid name digest algorithm: %w", err)
}
return subtle.ConstantTimeCompare(certifyInfo.Name.Buffer, name.Buffer) == 1, nil
}
func tpm2NameDigest(name tpm2.TPM2BName) (alg tpm2.TPMIAlgHash, digest []byte, err error) {
buf := name.Buffer
if len(buf) < 3 {
return 0, nil, fmt.Errorf("name too short")
}
alg = tpm2.TPMIAlgHash(binary.BigEndian.Uint16(buf[:2]))
var hash crypto.Hash
if hash, err = alg.Hash(); err != nil {
return 0, nil, fmt.Errorf("invalid hash algorithm: %w", err)
}
digest = buf[2:]
if len(digest) == 0 {
return 0, nil, fmt.Errorf("name digest is empty")
}
if len(digest) != hash.Size() {
return 0, nil, fmt.Errorf("invalid name digest length: %d", len(digest))
}
return alg, digest, nil
}
type tpm2AttStatement struct {
Version string
Algorithm int64
Signature []byte
CertInfo []byte
PubArea []byte
X5C []any
HasX5C bool
HasValidX5C bool
HasECDAAKeyID bool
HasValidECDAAKeyID bool
ECDAAKeyID []byte
}
func newTPM2AttStatement(raw map[string]any) (statement *tpm2AttStatement, err error) {
var ok bool
statement = &tpm2AttStatement{}
// 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.
if statement.Version, ok = raw[stmtVersion].(string); !ok {
return nil, ErrAttestationFormat.WithDetails("Error retrieving ver value")
}
if statement.Algorithm, ok = raw[stmtAlgorithm].(int64); !ok {
return nil, ErrAttestationFormat.WithDetails("Error retrieving alg value")
}
if statement.Signature, ok = raw[stmtSignature].([]byte); !ok {
return nil, ErrAttestationFormat.WithDetails("Error retrieving sig value")
}
if statement.CertInfo, ok = raw[stmtCertInfo].([]byte); !ok {
return nil, ErrAttestationFormat.WithDetails("Error retrieving certInfo value")
}
if statement.PubArea, ok = raw[stmtPubArea].([]byte); !ok {
return nil, ErrAttestationFormat.WithDetails("Error retrieving pubArea value")
}
var rawX5C, rawECDAAKeyID any
rawX5C, statement.HasX5C = raw[stmtX5C]
statement.X5C, statement.HasValidX5C = rawX5C.([]any)
rawECDAAKeyID, statement.HasECDAAKeyID = raw[stmtECDAAKID]
statement.ECDAAKeyID, statement.HasValidECDAAKeyID = rawECDAAKeyID.([]byte)
return statement, nil
}
// forEachSAN loops through the TPM SAN extension.
//
// 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 }
func forEachSAN(extension []byte, callback func(tag int, data []byte) error) error {
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
)
func parseSANExtension(value []byte) (manufacturer string, model string, version string, err error) {
err = forEachSAN(value, func(tag int, data []byte) error {
if tag == nameTypeDN {
tpmDeviceAttributes := pkix.RDNSequence{}
if _, err = asn1.Unmarshal(data, &tpmDeviceAttributes); 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(oidTCGAtTpmManufacturer) {
manufacturer = strings.TrimPrefix(value, "id:")
}
if atv.Type.Equal(oidTCGAtTpmModel) {
model = value
}
if atv.Type.Equal(oidTCGAtTPMVersion) {
version = strings.TrimPrefix(value, "id:")
}
}
}
}
return nil
})
return
}
// See https://trustedcomputinggroup.org/resource/vendor-id-registry/ for registry contents.
var tpmManufacturers = []struct {
id string
name string
code string
}{
{"414D4400", "AMD", "AMD"},
{"414E5400", "Ant Group", "ANT"},
{"41544D4C", "Atmel", "ATML"},
{"4252434D", "Broadcom", "BRCM"},
{"4353434F", "Cisco", "CSCO"},
{"464C5953", "Flyslice Technologies", "FLYS"},
{"524F4343", "Fuzhou Rockchip", "ROCC"},
{"474F4F47", "Google", "GOOG"},
{"48504900", "HPI", "HPI"},
{"48504500", "HPE", "HPE"},
{"48495349", "Huawei", "HISI"},
{"49424d00", "IBM", "IBM"},
{"49424D00", "IBM", "IBM"},
{"49465800", "Infineon", "IFX"},
{"494E5443", "Intel", "INTC"},
{"4C454E00", "Lenovo", "LEN"},
{"4D534654", "Microsoft", "MSFT"},
{"4E534D20", "National Semiconductor", "NSM"},
{"4E545A00", "Nationz", "NTZ"},
{"4E534700", "NSING", "NSG"},
{"4E544300", "Nuvoton Technology", "NTC"},
{"51434F4D", "Qualcomm", "QCOM"},
{"534D534E", "Samsung", "SECE"},
{"53454345", "SecEdge", "SecEdge"},
{"534E5300", "Sinosun", "SNS"},
{"534D5343", "SMSC", "SMSC"},
{"53544D20", "ST Microelectronics", "STM"},
{"54584E00", "Texas Instruments", "TXN"},
{"57454300", "Winbond", "WEC"},
{"5345414C", "Wisekey", "SEAL"},
{"FFFFF1D0", "FIDO Alliance Conformance Testing", "FIDO"},
}
func isValidTPMManufacturer(id string) bool {
for _, m := range tpmManufacturers {
if m.id == id {
return true
}
}
return false
}
func tpmParseAIKAttCA(x5c *x509.Certificate, x5cis []*x509.Certificate) (err *Error) {
if err = tpmParseSANExtension(x5c); err != nil {
return err
}
if err = tpmRemoveEKU(x5c); err != nil {
return err
}
for _, parent := range x5cis {
if err = tpmRemoveEKU(parent); err != nil {
return err
}
}
return nil
}
func tpmParseSANExtension(attestation *x509.Certificate) (protoErr *Error) {
var (
manufacturer, model, version string
err error
)
for _, ext := range attestation.Extensions {
if ext.Id.Equal(oidExtensionSubjectAltName) {
if manufacturer, model, version, err = parseSANExtension(ext.Value); err != nil {
return ErrInvalidAttestation.WithDetails("Authenticator with invalid Authenticator Identity Key SAN data encountered during attestation validation.").WithInfo(fmt.Sprintf("Error occurred parsing SAN extension: %s", err.Error())).WithError(err)
}
}
}
if manufacturer == "" || model == "" || version == "" {
return ErrAttestationFormat.WithDetails("Invalid SAN data in AIK certificate.")
}
var unhandled []asn1.ObjectIdentifier //nolint:prealloc
for _, uce := range attestation.UnhandledCriticalExtensions {
if uce.Equal(oidExtensionSubjectAltName) {
continue
}
unhandled = append(unhandled, uce)
}
attestation.UnhandledCriticalExtensions = unhandled
return nil
}
type tpmBasicConstraints struct {
IsCA bool `asn1:"optional"`
MaxPathLen int `asn1:"optional,default:-1"`
}
// Remove extension key usage to avoid ExtKeyUsage check failure.
func tpmRemoveEKU(x5c *x509.Certificate) *Error {
var (
unknown []asn1.ObjectIdentifier
hasAiK bool
)
for _, eku := range x5c.UnknownExtKeyUsage {
if eku.Equal(oidTCGKpAIKCertificate) {
hasAiK = true
continue
}
if eku.Equal(oidMicrosoftKpPrivacyCA) {
continue
}
unknown = append(unknown, eku)
}
if !hasAiK {
return ErrAttestationFormat.WithDetails("Attestation Identity Key certificate missing required Extended Key Usage.")
}
x5c.UnknownExtKeyUsage = unknown
return nil
}
func init() {
RegisterAttestationFormat(AttestationFormatTPM, attestationFormatValidationHandlerTPM)
}

View File

@@ -0,0 +1,426 @@
package protocol
import (
"bytes"
"encoding/binary"
"fmt"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
)
const (
minAuthDataLength = 37
minAttestedAuthLength = 55
maxCredentialIDLength = 1023
)
// AuthenticatorResponse represents the IDL with the same name.
//
// Authenticators respond to Relying Party requests by returning an object derived from the AuthenticatorResponse
// interface
//
// Specification: §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 represents the IDL with the same name.
//
// 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 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.
//
// Specification: §6.1. Authenticator Data (https://www.w3.org/TR/webauthn/#sctn-authenticator-data)
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. This is the CBOR representation of the
// credentials public key.
CredentialPublicKey []byte `json:"public_key"`
}
// CredentialMediationRequirement represents mediation requirements for clients. When making a request via get(options)
// or create(options), developers can set a case-by-case requirement for user mediation by choosing the appropriate
// CredentialMediationRequirement enum value.
//
// See https://www.w3.org/TR/credential-management-1/#mediation-requirements
type CredentialMediationRequirement string
const (
// MediationDefault lets the browser choose the mediation flow completely as if it wasn't specified at all.
MediationDefault CredentialMediationRequirement = ""
// MediationSilent indicates user mediation is suppressed for the given operation. If the operation can be performed
// without user involvement, wonderful. If user involvement is necessary, then the operation will return null rather
// than involving the user.
MediationSilent CredentialMediationRequirement = "silent"
// MediationOptional indicates if credentials can be handed over for a given operation without user mediation, they
// will be. If user mediation is required, then the user agent will involve the user in the decision.
MediationOptional CredentialMediationRequirement = "optional"
// MediationConditional indicates for get(), discovered credentials are presented to the user in a non-modal dialog
// along with an indication of the origin which is requesting credentials. If the user makes a gesture outside of
// the dialog, the dialog closes without resolving or rejecting the Promise returned by the get() method and without
// causing a user-visible error condition. If the user makes a gesture that selects a credential, that credential is
// returned to the caller. The prevent silent access flag is treated as being true regardless of its actual value:
// the conditional behavior always involves user mediation of some sort if applicable credentials are discovered.
MediationConditional CredentialMediationRequirement = "conditional"
// MediationRequired indicates the user agent will not hand over credentials without user mediation, even if the
// prevent silent access flag is unset for an origin.
MediationRequired CredentialMediationRequirement = "required"
)
// AuthenticatorAttachment represents the IDL enum of the same name, and is used as part of the Authenticator Selection
// Criteria.
//
// This enumerations values describe authenticators' attachment modalities. Relying Parties use this to express a
// preferred authenticator attachment modality when calling navigator.credentials.create() to create a credential.
//
// If this member is present, eligible authenticators are filtered to only authenticators attached with the specified
// §5.4.5 Authenticator Attachment Enumeration (enum AuthenticatorAttachment). The value SHOULD be a member of
// AuthenticatorAttachment but client platforms MUST ignore unknown values, treating an unknown value as if the member
// does not exist.
//
// Specification: §5.4.4. Authenticator Selection Criteria (https://www.w3.org/TR/webauthn/#dom-authenticatorselectioncriteria-authenticatorattachment)
//
// Specification: §5.4.5. Authenticator Attachment Enumeration (https://www.w3.org/TR/webauthn/#enum-attachment)
type AuthenticatorAttachment string
const (
// Platform represents 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 represents 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"
)
// ResidentKeyRequirement represents the IDL of the same name.
//
// This enumerations values describe the Relying Party's requirements for client-side discoverable credentials
// (formerly known as resident credentials or resident keys).
//
// Specifies the extent to which the Relying Party desires to create a client-side discoverable credential. For
// historical reasons the naming retains the deprecated “resident” terminology. The value SHOULD be a member of
// ResidentKeyRequirement but client platforms MUST ignore unknown values, treating an unknown value as if the member
// does not exist. If no value is given then the effective value is required if requireResidentKey is true or
// discouraged if it is false or absent.
//
// Specification: §5.4.4. Authenticator Selection Criteria (https://www.w3.org/TR/webauthn/#dom-authenticatorselectioncriteria-residentkey)
//
// Specification: §5.4.6. Resident Key Requirement Enumeration (https://www.w3.org/TR/webauthn/#enumdef-residentkeyrequirement)
type ResidentKeyRequirement string
const (
// ResidentKeyRequirementDiscouraged indicates the Relying Party prefers creating a server-side credential, but will
// accept a client-side discoverable credential. This is the default.
ResidentKeyRequirementDiscouraged ResidentKeyRequirement = "discouraged"
// ResidentKeyRequirementPreferred indicates to the client we would prefer a discoverable credential.
ResidentKeyRequirementPreferred ResidentKeyRequirement = "preferred"
// ResidentKeyRequirementRequired indicates the Relying Party requires a client-side discoverable credential, and is
// prepared to receive an error if a client-side discoverable credential cannot be created.
ResidentKeyRequirementRequired ResidentKeyRequirement = "required"
)
// AuthenticatorTransport represents the IDL enum with the same name.
//
// 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 will typically learn of the supported transports for a public key credential via
// getTransports().
//
// Specification: §5.8.4. Authenticator Transport Enumeration (https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport)
type AuthenticatorTransport string
const (
// USB indicates the respective authenticator can be contacted over removable USB.
USB AuthenticatorTransport = "usb"
// NFC indicates the respective authenticator can be contacted over Near Field Communication (NFC).
NFC AuthenticatorTransport = "nfc"
// BLE indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low Energy / BLE).
BLE AuthenticatorTransport = "ble"
// SmartCard indicates the respective authenticator can be contacted over ISO/IEC 7816 smart card with contacts.
//
// WebAuthn Level 3.
SmartCard AuthenticatorTransport = "smart-card"
// Hybrid indicates the respective authenticator can be contacted using a combination of (often separate)
// data-transport and proximity mechanisms. This supports, for example, authentication on a desktop computer using
// a smartphone.
//
// WebAuthn Level 3.
Hybrid AuthenticatorTransport = "hybrid"
// Internal indicates the respective authenticator is contacted using a client device-specific transport, i.e., it
// is a platform authenticator. These authenticators are not removable from the client device.
Internal AuthenticatorTransport = "internal"
)
// UserVerificationRequirement is a representation of the UserVerificationRequirement IDL enum.
//
// 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.
//
// Specification: §5.8.6. User Verification Requirement Enumeration (https://www.w3.org/TR/webauthn/#enum-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.
//
// Specification: §6.1. Authenticator Data - Flags (https://www.w3.org/TR/webauthn/#flags)
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. Also referred to as the UP flag.
FlagUserPresent AuthenticatorFlags = 1 << iota // Referred to as UP.
// FlagRFU1 is a reserved for future use flag.
FlagRFU1
// FlagUserVerified Bit 00000100 in the byte sequence. Tells us if user is verified
// by the authenticator using a biometric or PIN. Also referred to as the UV flag.
FlagUserVerified
// FlagBackupEligible Bit 00001000 in the byte sequence. Tells us if a backup is eligible for device. Also referred
// to as the BE flag.
FlagBackupEligible // Referred to as BE.
// FlagBackupState Bit 00010000 in the byte sequence. Tells us if a backup state for device. Also referred to as the
// BS flag.
FlagBackupState
// FlagRFU2 is a reserved for future use flag.
FlagRFU2
// FlagAttestedCredentialData Bit 01000000 in the byte sequence. Indicates whether
// the authenticator added attested credential data. Also referred to as the AT flag.
FlagAttestedCredentialData
// FlagHasExtensions Bit 10000000 in the byte sequence. Indicates if the authenticator data has extensions. Also
// referred to as the ED flag.
FlagHasExtensions
)
// UserPresent returns if the UP flag was set.
func (flag AuthenticatorFlags) UserPresent() bool {
return flag.HasUserPresent()
}
// UserVerified returns if the UV flag was set.
func (flag AuthenticatorFlags) UserVerified() bool {
return flag.HasUserVerified()
}
// HasUserPresent returns if the UP flag was set.
func (flag AuthenticatorFlags) HasUserPresent() bool {
return (flag & FlagUserPresent) == FlagUserPresent
}
// HasUserVerified returns if the UV flag was set.
func (flag AuthenticatorFlags) HasUserVerified() 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
}
// HasBackupEligible returns if the BE flag was set.
func (flag AuthenticatorFlags) HasBackupEligible() bool {
return (flag & FlagBackupEligible) == FlagBackupEligible
}
// HasBackupState returns if the BS flag was set.
func (flag AuthenticatorFlags) HasBackupState() bool {
return (flag & FlagBackupState) == FlagBackupState
}
// Unmarshal will take the raw Authenticator Data and marshals 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) (err error) {
if minAuthDataLength > len(rawAuthData) {
return ErrBadRequest.
WithDetails("Authenticator data length too short").
WithInfo(fmt.Sprintf("Expected data greater than %d bytes. Got %d bytes", minAuthDataLength, len(rawAuthData)))
}
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) > minAttestedAuthLength {
if err = a.unmarshalAttestedData(rawAuthData); err != nil {
return err
}
attDataLen := len(a.AttData.AAGUID) + 2 + len(a.AttData.CredentialID) + len(a.AttData.CredentialPublicKey)
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) (err error) {
a.AttData.AAGUID = rawAuthData[37:53]
idLength := binary.BigEndian.Uint16(rawAuthData[53:55])
if len(rawAuthData) < int(55+idLength) {
return ErrBadRequest.WithDetails("Authenticator attestation data length too short")
}
if idLength > maxCredentialIDLength {
return ErrBadRequest.WithDetails("Authenticator attestation data credential id length too long")
}
a.AttData.CredentialID = rawAuthData[55 : 55+idLength]
a.AttData.CredentialPublicKey, err = unmarshalCredentialPublicKey(rawAuthData[55+idLength:])
if err != nil {
return ErrBadRequest.WithDetails(fmt.Sprintf("Could not unmarshal Credential Public Key: %v", err)).WithError(err)
}
return nil
}
// Unmarshall the credential's Public Key into CBOR encoding.
func unmarshalCredentialPublicKey(keyBytes []byte) (rawBytes []byte, err error) {
var m any
if err = webauthncbor.Unmarshal(keyBytes, &m); err != nil {
return nil, err
}
if rawBytes, err = webauthncbor.Marshal(m); err != nil {
return nil, err
}
return rawBytes, nil
}
// ResidentKeyRequired - Require that the key be private key resident to the client device.
func ResidentKeyRequired() *bool {
required := true
return &required
}
// ResidentKeyNotRequired - Do not require that the private key be resident to the client device.
func ResidentKeyNotRequired() *bool {
required := false
return &required
}
// Verify on AuthenticatorData handles Steps 13 through 15 & 17 for Registration
// and Steps 15 through 18 for Assertion.
func (a *AuthenticatorData) Verify(rpIdHash []byte, appIDHash []byte, userVerificationRequired bool, userPresenceRequired bool) (err error) {
// Registration Step 13 & Assertion Step 15
// 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 %x and Received %x", a.RPIDHash, rpIdHash))
}
// Registration Step 15 & Assertion Step 16
// Verify that the User Present bit of the flags in authData is set.
if userPresenceRequired && !a.Flags.UserPresent() {
return ErrVerification.WithInfo("User presence required but flag not set by authenticator")
}
// Registration Step 15 & Assertion Step 17
// 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("User verification required but flag not set by authenticator")
}
// Registration Step 17 & Assertion Step 18
// 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
}

View File

@@ -0,0 +1,52 @@
package protocol
import (
"bytes"
"encoding/base64"
"reflect"
)
// URLEncodedBase64 represents a byte slice holding URL-encoded base64 data.
// When fields of this type are unmarshalled from JSON, the data is base64
// decoded into a byte slice.
type URLEncodedBase64 []byte
func (e URLEncodedBase64) String() string {
return base64.RawURLEncoding.EncodeToString(e)
}
// UnmarshalJSON base64 decodes a URL-encoded value, storing the result in the
// provided byte slice.
func (e *URLEncodedBase64) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("null")) {
return nil
}
// Trim the leading and trailing quotes from raw JSON data (the whole value part).
data = bytes.Trim(data, `"`)
// Trim the trailing equal characters.
data = bytes.TrimRight(data, "=")
out := make([]byte, base64.RawURLEncoding.DecodedLen(len(data)))
n, err := base64.RawURLEncoding.Decode(out, data)
if err != nil {
return err
}
v := reflect.ValueOf(e).Elem()
v.SetBytes(out[:n])
return nil
}
// MarshalJSON base64 encodes a non URL-encoded value, storing the result in the
// provided byte slice.
func (e URLEncodedBase64) MarshalJSON() ([]byte, error) {
if e == nil {
return []byte("null"), nil
}
return []byte(`"` + base64.RawURLEncoding.EncodeToString(e) + `"`), nil
}

View File

@@ -0,0 +1,20 @@
package protocol
import (
"crypto/rand"
)
// ChallengeLength - Length of bytes to generate for a challenge.
const ChallengeLength = 32
// CreateChallenge creates a new challenge that should be signed and returned by the authenticator. The spec recommends
// using at least 16 bytes with 100 bits of entropy. We use 32 bytes.
func CreateChallenge() (challenge URLEncodedBase64, err error) {
challenge = make([]byte, ChallengeLength)
if _, err = rand.Read(challenge); err != nil {
return nil, err
}
return challenge, nil
}

View File

@@ -0,0 +1,285 @@
package protocol
import (
"crypto/subtle"
"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.
//
// Specification: §5.8.1. Client Data Used in WebAuthn Signatures (https://www.w3.org/TR/webauthn/#dictdef-collectedclientdata)
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"`
TopOrigin string `json:"topOrigin,omitempty"`
CrossOrigin bool `json:"crossOrigin,omitempty"`
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 (
// Present indicates token binding was used when communicating with the
// Relying Party. In this case, the id member MUST be present.
Present TokenBindingStatus = "present"
// Supported indicates token binding was used when communicating with the
// negotiated when communicating with the Relying Party.
Supported TokenBindingStatus = "supported"
// NotSupported indicates token binding not supported
// when communicating with the Relying Party.
NotSupported TokenBindingStatus = "not-supported"
)
// FullyQualifiedOrigin returns the origin per the HTML spec: (scheme)://(host)[:(port)].
func FullyQualifiedOrigin(rawOrigin string) (fqOrigin string, err error) {
if strings.HasPrefix(rawOrigin, "android:apk-key-hash:") {
return rawOrigin, nil
}
var origin *url.URL
if origin, err = url.ParseRequestURI(rawOrigin); err != nil {
return "", err
}
if origin.Host == "" {
return "", fmt.Errorf("url '%s' does not have a host", rawOrigin)
}
origin.Path, origin.RawPath, origin.RawQuery, origin.User = "", "", "", nil
return origin.String(), nil
}
// Verify handles steps 3 through 6 of verifying 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
//
// Note: the rpTopOriginsVerify parameter does not accept the TopOriginVerificationMode value of
// TopOriginDefaultVerificationMode as it's expected this value is updated by the config validation process.
func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyType, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode) (err 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 {
return ErrVerification.WithDetails("Error validating ceremony type").WithInfo(fmt.Sprintf("Expected Value: %s, Received: %s", ceremony, c.Type))
}
// 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 subtle.ConstantTimeCompare([]byte(storedChallenge), []byte(challenge)) != 1 {
return ErrVerification.
WithDetails("Error validating challenge").
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.
if !IsOriginInHaystack(c.Origin, rpOrigins) {
return ErrVerification.
WithDetails("Error validating origin").
WithInfo(fmt.Sprintf("Expected Values: %s, Received: %s", rpOrigins, c.Origin))
}
if rpTopOriginsVerify != TopOriginIgnoreVerificationMode {
switch len(c.TopOrigin) {
case 0:
break
default:
if !c.CrossOrigin {
return ErrVerification.
WithDetails("Error validating topOrigin").
WithInfo("The topOrigin can't have values unless crossOrigin is true.")
}
var (
fqTopOrigin string
possibleTopOrigins []string
)
switch rpTopOriginsVerify {
case TopOriginExplicitVerificationMode:
possibleTopOrigins = rpTopOrigins
case TopOriginAutoVerificationMode:
possibleTopOrigins = append(rpTopOrigins, rpOrigins...) //nolint:gocritic // This is intentional.
case TopOriginImplicitVerificationMode:
possibleTopOrigins = rpOrigins
default:
return ErrNotImplemented.WithDetails("Error handling unknown Top Origin verification mode")
}
if !IsOriginInHaystack(c.TopOrigin, possibleTopOrigins) {
return ErrVerification.
WithDetails("Error validating top origin").
WithInfo(fmt.Sprintf("Expected Values: %s, Received: %s", possibleTopOrigins, fqTopOrigin))
}
}
}
// 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", c.TokenBinding.Status))
}
}
// Not yet fully implemented by the spec, browsers, and me.
return nil
}
type TopOriginVerificationMode int
const (
// TopOriginDefaultVerificationMode represents the default verification mode for the Top Origin. At this time this
// mode is the same as TopOriginIgnoreVerificationMode until such a time as the specification becomes stable. This
// value is intended as a fallback value and implementers should very intentionally pick another option if they want
// stability.
TopOriginDefaultVerificationMode TopOriginVerificationMode = iota
// TopOriginIgnoreVerificationMode ignores verification entirely.
TopOriginIgnoreVerificationMode
// TopOriginAutoVerificationMode represents the automatic verification mode for the Top Origin. In this mode the
// If the Top Origins parameter has values it checks against this, otherwise it checks against the Origins parameter.
TopOriginAutoVerificationMode
// TopOriginImplicitVerificationMode represents the implicit verification mode for the Top Origin. In this mode the
// Top Origin is verified against the allowed Origins values.
TopOriginImplicitVerificationMode
// TopOriginExplicitVerificationMode represents the explicit verification mode for the Top Origin. In this mode the
// Top Origin is verified against the allowed Top Origins values.
TopOriginExplicitVerificationMode
)
// IsOriginInHaystack checks if the needle is in the haystack using the mechanism to determine origin equality defined
// in HTML5 Section 5.3 and RFC3986 Section 6.2.1.
//
// Specifically if the needle value has the 'http://' or 'https://' prefix (case-insensitive) and can be parsed as a
// URL; we check each item in the haystack to see if it matches the same rules, and then if the scheme and host (with
// a normalized port) components match case-insensitively then they're considered a match.
//
// If the needle value does not have the 'http://' or 'https://' prefix (case-insensitive) or can't be parsed as a URL
// equality is determined using simple string comparison.
//
// It is important to note that this function completely ignores Apple Associated Domains entirely as Apple is using
// an unassigned Well-Known URI in breech of Well-Known Uniform Resource Identifiers (RFC8615).
//
// See (Origin Definition): https://www.w3.org/TR/2011/WD-html5-20110525/origin-0.html
//
// See (Simple String Comparison Definition): https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.1
//
// See (Apple Associated Domains): https://developer.apple.com/documentation/xcode/supporting-associated-domains
//
// See (IANA Well Known URI Assignments): https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml
//
// See (Well-Known Uniform Resource Identifiers): https://datatracker.ietf.org/doc/html/rfc8615
func IsOriginInHaystack(needle string, haystack []string) bool {
needleURI := parseOriginURI(needle)
if needleURI != nil {
for _, hay := range haystack {
if hayURI := parseOriginURI(hay); hayURI != nil {
if isOriginEqual(needleURI, hayURI) {
return true
}
}
}
} else {
for _, hay := range haystack {
if needle == hay {
return true
}
}
}
return false
}
func isOriginEqual(a *url.URL, b *url.URL) bool {
if !strings.EqualFold(a.Scheme, b.Scheme) {
return false
}
if !strings.EqualFold(a.Host, b.Host) {
return false
}
return true
}
func parseOriginURI(raw string) *url.URL {
if !isPossibleFQDN(raw) {
return nil
}
// We can ignore the error here because it's effectively not a FQDN if this fails.
uri, _ := url.Parse(raw)
if uri == nil {
return nil
}
// Normalize the port if necessary.
switch uri.Scheme {
case "http":
if uri.Port() == "80" {
uri.Host = uri.Hostname()
}
case "https":
if uri.Port() == "443" {
uri.Host = uri.Hostname()
}
}
return uri
}
func isPossibleFQDN(raw string) bool {
normalized := strings.ToLower(raw)
return strings.HasPrefix(normalized, "http://") || strings.HasPrefix(normalized, "https://")
}

View File

@@ -0,0 +1,226 @@
package protocol
import "encoding/asn1"
const (
stmtAttStmt = "attStmt"
stmtFmt = "fmt"
stmtX5C = "x5c"
stmtSignature = "sig"
stmtAlgorithm = "alg"
stmtVersion = "ver"
stmtECDAAKID = "ecdaaKeyId"
stmtCertInfo = "certInfo"
stmtPubArea = "pubArea"
)
const (
versionTPM20 = "2.0"
)
const (
attStatementAndroidSafetyNetHostname = "attest.android.com"
)
var (
// internalRemappedAuthenticatorTransport handles remapping of AuthenticatorTransport values. Specifically it is
// intentional on remapping only transports that never made recommendation but are being used in the wild. It
// should not be used to handle transports that were ratified.
internalRemappedAuthenticatorTransport = map[string]AuthenticatorTransport{
// The Authenticator Transport 'hybrid' was previously named 'cable'; even if it was for a short period.
"cable": Hybrid,
}
)
const (
/*
Apple Anonymous Attestation Root 1 in PEM form.
Source: https://www.apple.com/certificateauthority/Apple_WebAuthn_Root_CA.pem
SHA256 Fingerprints:
Root 1: 09:15:DD:5C:07:A2:8D:B5:49:D1:F6:77:BB:5A:75:D4:BF:BE:95:61:A7:73:42:43:27:76:2E:9E:02:F9:BB:29
*/
certificateAppleRoot1 = `-----BEGIN CERTIFICATE-----
MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
1bWeT0vT
-----END CERTIFICATE-----`
)
const (
/*
Google Hardware Attestation Root 1 through Root 5 in PEM form.
Source: https://developer.android.com/training/articles/security-key-attestation#root_certificate
SHA256 Fingerprints:
Root 1: CE:DB:1C:B6:DC:89:6A:E5:EC:79:73:48:BC:E9:28:67:53:C2:B3:8E:E7:1C:E0:FB:E3:4A:9A:12:48:80:0D:FC
Root 2: 6D:9D:B4:CE:6C:5C:0B:29:31:66:D0:89:86:E0:57:74:A8:77:6C:EB:52:5D:9E:43:29:52:0D:E1:2B:A4:BC:C0
Root 3: C1:98:4A:3E:F4:5C:1E:2A:91:85:51:DE:10:60:3C:86:F7:05:1B:22:49:C4:89:1C:AE:32:30:EA:BD:0C:97:D5
Root 4: 1E:F1:A0:4B:8B:A5:8A:B9:45:89:AC:49:8C:89:82:A7:83:F2:4E:A7:30:7E:01:59:A0:C3:A7:3B:37:7D:87:CC
Root 5: AB:66:41:17:8A:36:E1:79:AA:0C:1C:DD:DF:9A:16:EB:45:FA:20:94:3E:2B:8C:D7:C7:C0:5C:26:CF:8B:48:7A
*/
certificateAndroidKeyRoot1 = `-----BEGIN CERTIFICATE-----
MIIFHDCCAwSgAwIBAgIJAPHBcqaZ6vUdMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMjIwMzIwMTgwNzQ4WhcNNDIwMzE1MTgw
NzQ4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud
IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD
VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQB8cMqTllHc8U+qCrOlg3H7
174lmaCsbo/bJ0C17JEgMLb4kvrqsXZs01U3mB/qABg/1t5Pd5AORHARs1hhqGIC
W/nKMav574f9rZN4PC2ZlufGXb7sIdJpGiO9ctRhiLuYuly10JccUZGEHpHSYM2G
tkgYbZba6lsCPYAAP83cyDV+1aOkTf1RCp/lM0PKvmxYN10RYsK631jrleGdcdkx
oSK//mSQbgcWnmAEZrzHoF1/0gso1HZgIn0YLzVhLSA/iXCX4QT2h3J5z3znluKG
1nv8NQdxei2DIIhASWfu804CA96cQKTTlaae2fweqXjdN1/v2nqOhngNyz1361mF
mr4XmaKH/ItTwOe72NI9ZcwS1lVaCvsIkTDCEXdm9rCNPAY10iTunIHFXRh+7KPz
lHGewCq/8TOohBRn0/NNfh7uRslOSZ/xKbN9tMBtw37Z8d2vvnXq/YWdsm1+JLVw
n6yYD/yacNJBlwpddla8eaVMjsF6nBnIgQOf9zKSe06nSTqvgwUHosgOECZJZ1Eu
zbH4yswbt02tKtKEFhx+v+OTge/06V+jGsqTWLsfrOCNLuA8H++z+pUENmpqnnHo
vaI47gC+TNpkgYGkkBT6B/m/U01BuOBBTzhIlMEZq9qkDWuM2cA5kW5V3FJUcfHn
w1IdYIg2Wxg7yHcQZemFQg==
-----END CERTIFICATE-----`
certificateAndroidKeyRoot2 = `-----BEGIN CERTIFICATE-----
MIICIjCCAaigAwIBAgIRAISp0Cl7DrWK5/8OgN52BgUwCgYIKoZIzj0EAwMwUjEc
MBoGA1UEAwwTS2V5IEF0dGVzdGF0aW9uIENBMTEQMA4GA1UECwwHQW5kcm9pZDET
MBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMCVVMwHhcNMjUwNzE3MjIzMjE4
WhcNMzUwNzE1MjIzMjE4WjBSMRwwGgYDVQQDDBNLZXkgQXR0ZXN0YXRpb24gQ0Ex
MRAwDgYDVQQLDAdBbmRyb2lkMRMwEQYDVQQKDApHb29nbGUgTExDMQswCQYDVQQG
EwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABCPaI3FO3z5bBQo8cuiEas4HjqCt
G/mLFfRT0MsIssPBEEU5Cfbt6sH5yOAxqEi5QagpU1yX4HwnGb7OtBYpDTB57uH5
Eczm34A5FNijV3s0/f0UPl7zbJcTx6xwqMIRq6NCMEAwDwYDVR0TAQH/BAUwAwEB
/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFIyuyz7RkOb3NaBqQ5lZuA0QepA
MAoGCCqGSM49BAMDA2gAMGUCMETfjPO/HwqReR2CS7p0ZWoD/LHs6hDi422opifH
EUaYLxwGlT9SLdjkVpz0UUOR5wIxAIoGyxGKRHVTpqpGRFiJtQEOOTp/+s1GcxeY
uR2zh/80lQyu9vAFCj6E4AXc+osmRg==
-----END CERTIFICATE-----`
certificateAndroidKeyRoot3 = `-----BEGIN CERTIFICATE-----
MIIFYDCCA0igAwIBAgIJAOj6GWMU0voYMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTYwNTI2MTYyODUyWhcNMjYwNTI0MTYy
ODUyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
AGMCAwEAAaOBpjCBozAdBgNVHQ4EFgQUNmHhAHyIBQlRi0RsR/8aTMnqTxIwHwYD
VR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB/zAO
BgNVHQ8BAf8EBAMCAYYwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cHM6Ly9hbmRyb2lk
Lmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8wDQYJKoZIhvcNAQELBQAD
ggIBACDIw41L3KlXG0aMiS//cqrG+EShHUGo8HNsw30W1kJtjn6UBwRM6jnmiwfB
Pb8VA91chb2vssAtX2zbTvqBJ9+LBPGCdw/E53Rbf86qhxKaiAHOjpvAy5Y3m00m
qC0w/Zwvju1twb4vhLaJ5NkUJYsUS7rmJKHHBnETLi8GFqiEsqTWpG/6ibYCv7rY
DBJDcR9W62BW9jfIoBQcxUCUJouMPH25lLNcDc1ssqvC2v7iUgI9LeoM1sNovqPm
QUiG9rHli1vXxzCyaMTjwftkJLkf6724DFhuKug2jITV0QkXvaJWF4nUaHOTNA4u
JU9WDvZLI1j83A+/xnAJUucIv/zGJ1AMH2boHqF8CY16LpsYgBt6tKxxWH00XcyD
CdW2KlBCeqbQPcsFmWyWugxdcekhYsAWyoSf818NUsZdBWBaR/OukXrNLfkQ79Iy
ZohZbvabO/X+MVT3rriAoKc8oE2Uws6DF+60PV7/WIPjNvXySdqspImSN78mflxD
qwLqRBYkA3I75qppLGG9rp7UCdRjxMl8ZDBld+7yvHVgt1cVzJx9xnyGCC23Uaic
MDSXYrB4I4WHXPGjxhZuCuPBLTdOLU8YRvMYdEvYebWHMpvwGCF6bAx3JBpIeOQ1
wDB5y0USicV3YgYGmi+NZfhA4URSh77Yd6uuJOJENRaNVTzk
-----END CERTIFICATE-----`
certificateAndroidKeyRoot4 = `-----BEGIN CERTIFICATE-----
MIIFHDCCAwSgAwIBAgIJANUP8luj8tazMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTkxMTIyMjAzNzU4WhcNMzQxMTE4MjAz
NzU4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud
IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD
VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBOMaBc8oumXb2voc7XCWnu
XKhBBK3e2KMGz39t7lA3XXRe2ZLLAkLM5y3J7tURkf5a1SutfdOyXAmeE6SRo83U
h6WszodmMkxK5GM4JGrnt4pBisu5igXEydaW7qq2CdC6DOGjG+mEkN8/TA6p3cno
L/sPyz6evdjLlSeJ8rFBH6xWyIZCbrcpYEJzXaUOEaxxXxgYz5/cTiVKN2M1G2ok
QBUIYSY6bjEL4aUN5cfo7ogP3UvliEo3Eo0YgwuzR2v0KR6C1cZqZJSTnghIC/vA
D32KdNQ+c3N+vl2OTsUVMC1GiWkngNx1OO1+kXW+YTnnTUOtOIswUP/Vqd5SYgAI
mMAfY8U9/iIgkQj6T2W6FsScy94IN9fFhE1UtzmLoBIuUFsVXJMTz+Jucth+IqoW
Fua9v1R93/k98p41pjtFX+H8DslVgfP097vju4KDlqN64xV1grw3ZLl4CiOe/A91
oeLm2UHOq6wn3esB4r2EIQKb6jTVGu5sYCcdWpXr0AUVqcABPdgL+H7qJguBw09o
jm6xNIrw2OocrDKsudk/okr/AwqEyPKw9WnMlQgLIKw1rODG2NvU9oR3GVGdMkUB
ZutL8VuFkERQGt6vQ2OCw0sV47VMkuYbacK/xyZFiRcrPJPb41zgbQj9XAEyLKCH
ex0SdDrx+tWUDqG8At2JHA==
-----END CERTIFICATE-----`
certificateAndroidKeyRoot5 = `-----BEGIN CERTIFICATE-----
MIIFHDCCAwSgAwIBAgIJAMNrfES5rhgxMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMjExMTE3MjMxMDQyWhcNMzYxMTEzMjMx
MDQyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud
IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD
VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBTNNZe5cuf8oiq+jV0itTG
zWVhSTjOBEk2FQvh11J3o3lna0o7rd8RFHnN00q4hi6TapFhh4qaw/iG6Xg+xOan
63niLWIC5GOPFgPeYXM9+nBb3zZzC8ABypYuCusWCmt6Tn3+Pjbz3MTVhRGXuT/T
QH4KGFY4PhvzAyXwdjTOCXID+aHud4RLcSySr0Fq/L+R8TWalvM1wJJPhyRjqRCJ
erGtfBagiALzvhnmY7U1qFcS0NCnKjoO7oFedKdWlZz0YAfu3aGCJd4KHT0MsGiL
Zez9WP81xYSrKMNEsDK+zK5fVzw6jA7cxmpXcARTnmAuGUeI7VVDhDzKeVOctf3a
0qQLwC+d0+xrETZ4r2fRGNw2YEs2W8Qj6oDcfPvq9JySe7pJ6wcHnl5EZ0lwc4xH
7Y4Dx9RA1JlfooLMw3tOdJZH0enxPXaydfAD3YifeZpFaUzicHeLzVJLt9dvGB0b
HQLE4+EqKFgOZv2EoP686DQqbVS1u+9k0p2xbMA105TBIk7npraa8VM0fnrRKi7w
lZKwdH+aNAyhbXRW9xsnODJ+g8eF452zvbiKKngEKirK5LGieoXBX7tZ9D1GNBH2
Ob3bKOwwIWdEFle/YF/h6zWgdeoaNGDqVBrLr2+0DtWoiB1aDEjLWl9FmyIUyUm7
mD/vFDkzF+wm7cyWpQpCVQ==
-----END CERTIFICATE-----`
)
var (
oidExtensionAppleAnonymousAttestation = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 2}
oidExtensionAndroidKeystore = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 1, 17}
oidExtensionSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17}
oidExtensionExtendedKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37}
oidExtensionBasicConstraints = asn1.ObjectIdentifier{2, 5, 29, 19}
oidFIDOGenCeAAGUID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 45724, 1, 1, 4}
oidMicrosoftKpPrivacyCA = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 21, 36}
oidTCGKpAIKCertificate = asn1.ObjectIdentifier{2, 23, 133, 8, 3}
oidTCGAtTpmManufacturer = asn1.ObjectIdentifier{2, 23, 133, 2, 1}
oidTCGAtTpmModel = asn1.ObjectIdentifier{2, 23, 133, 2, 2}
oidTCGAtTPMVersion = asn1.ObjectIdentifier{2, 23, 133, 2, 3}
)

View File

@@ -0,0 +1,265 @@
package protocol
import (
"crypto/sha256"
"encoding/base64"
"io"
"net/http"
"github.com/go-webauthn/webauthn/metadata"
)
// Credential is the basic credential type from the Credential Management specification that is inherited by WebAuthn's
// PublicKeyCredential type.
//
// Specification: Credential Management §2.2. The Credential Interface (https://www.w3.org/TR/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"`
}
// ParsedCredential is the parsed 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"`
AuthenticatorAttachment string `json:"authenticatorAttachment,omitempty"`
}
type ParsedPublicKeyCredential struct {
ParsedCredential
RawID []byte `json:"rawId"`
ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
AuthenticatorAttachment AuthenticatorAttachment `json:"authenticatorAttachment,omitempty"`
}
type CredentialCreationResponse struct {
PublicKeyCredential
AttestationResponse AuthenticatorAttestationResponse `json:"response"`
}
type ParsedCredentialCreationData struct {
ParsedPublicKeyCredential
Response ParsedAttestationResponse
Raw CredentialCreationResponse
}
// ParseCredentialCreationResponse is a non-agnostic function for parsing a registration response from the http library
// from stdlib. It handles some standard cleanup operations.
func ParseCredentialCreationResponse(request *http.Request) (*ParsedCredentialCreationData, error) {
if request == nil || request.Body == nil {
return nil, ErrBadRequest.WithDetails("No response given")
}
defer func() {
_, _ = io.Copy(io.Discard, request.Body)
_ = request.Body.Close()
}()
return ParseCredentialCreationResponseBody(request.Body)
}
// ParseCredentialCreationResponseBody is an agnostic version of ParseCredentialCreationResponse. Implementers are
// therefore responsible for managing cleanup.
func ParseCredentialCreationResponseBody(body io.Reader) (pcc *ParsedCredentialCreationData, err error) {
var ccr CredentialCreationResponse
if err = decodeBody(body, &ccr); err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo(err.Error()).WithError(err)
}
return ccr.Parse()
}
// ParseCredentialCreationResponseBytes is an alternative version of ParseCredentialCreationResponseBody that just takes
// a byte slice.
func ParseCredentialCreationResponseBytes(data []byte) (pcc *ParsedCredentialCreationData, err error) {
var ccr CredentialCreationResponse
if err = decodeBytes(data, &ccr); err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo(err.Error()).WithError(err)
}
return ccr.Parse()
}
// Parse validates and parses the CredentialCreationResponse into a ParsedCredentialCreationData. This receiver
// is unlikely to be expressly guaranteed under the versioning policy. Users looking for this guarantee should see
// ParseCredentialCreationResponseBody instead, and this receiver should only be used if that function is inadequate
// for their use case.
func (ccr CredentialCreationResponse) Parse() (pcc *ParsedCredentialCreationData, 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.Type == "" {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing type")
}
if ccr.Type != string(PublicKeyCredentialType) {
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Type not public-key")
}
response, err := ccr.AttestationResponse.Parse()
if err != nil {
return nil, ErrParsingData.WithDetails("Error parsing attestation response")
}
var attachment AuthenticatorAttachment
switch ccr.AuthenticatorAttachment {
case "platform":
attachment = Platform
case "cross-platform":
attachment = CrossPlatform
}
return &ParsedCredentialCreationData{
ParsedPublicKeyCredential{
ParsedCredential{ccr.ID, ccr.Type}, ccr.RawID, ccr.ClientExtensionResults, attachment,
},
*response,
ccr,
}, nil
}
// Verify the Client and Attestation data.
//
// Specification: §7.1. Registering a New Credential (https://www.w3.org/TR/webauthn/#sctn-registering-a-new-credential)
func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, verifyUserPresence bool, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode, mds metadata.Provider, credParams []CredentialParameter) (clientDataHash []byte, err error) {
// Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data.
if err = pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify); err != nil {
return nil, err
}
// Step 7. Compute the hash of response.clientDataJSON using SHA-256.
sum := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON)
clientDataHash = sum[:]
// 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.
// We do the above step while parsing and decoding the CredentialCreationResponse
// Handle steps 9 through 14 - This verifies the attestation object.
if err = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash, verifyUser, verifyUserPresence, mds, credParams); err != nil {
return clientDataHash, err
}
// 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 clientDataHash, 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 if it hasn't return an error.
// 2. Check that the AuthenticationExtensionsClientOutputs contains the extensions output and return an empty string if it doesn't.
// 3. Check that the Credential AttestationType is `fido-u2f` and return an empty string if it isn't.
// 4. Check that the AuthenticationExtensionsClientOutputs contains the appid key and if it doesn't return an empty string.
// 5. Check that the AuthenticationExtensionsClientOutputs appid is a bool and if it isn't return an error.
// 6. Check that the appid output is true and if it isn't return an empty string.
// 7. Check that the Session Data has an appid extension defined and if it doesn't return an error.
// 8. Check that the appid extension in Session Data is a string and if it isn't return an error.
// 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://www.w3.org/TR/webauthn/#sctn-fido-u2f-attestation
if credentialAttestationType != CredentialTypeFIDOU2F {
return "", nil
}
if clientValue, ok = ppkc.ClientExtensionResults[ExtensionAppID]; !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[ExtensionAppID]; !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
}
const (
CredentialTypeFIDOU2F = "fido-u2f"
)

View File

@@ -0,0 +1,40 @@
package protocol
import (
"bytes"
"encoding/json"
"errors"
"io"
)
func decodeBody(body io.Reader, v any) (err error) {
decoder := json.NewDecoder(body)
if err = decoder.Decode(v); err != nil {
return err
}
_, err = decoder.Token()
if !errors.Is(err, io.EOF) {
return errors.New("body contains trailing data")
}
return nil
}
func decodeBytes(data []byte, v any) (err error) {
decoder := json.NewDecoder(bytes.NewReader(data))
if err = decoder.Decode(v); err != nil {
return err
}
_, err = decoder.Token()
if !errors.Is(err, io.EOF) {
return errors.New("body contains trailing data")
}
return nil
}

View File

@@ -0,0 +1,8 @@
// Package protocol contains data structures and validation functionality
// outlined in the Web Authentication 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

View File

@@ -0,0 +1,46 @@
package protocol
// CredentialEntity represents the PublicKeyCredentialEntity IDL and it describes a user account, or a WebAuthn Relying
// Party with which a public key credential is associated.
//
// Specification: §5.4.1. Public Key Entity Description (https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity)
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"`
}
// The RelyingPartyEntity represents the PublicKeyCredentialRpEntity IDL and is used to supply additional Relying Party
// attributes when creating a new credential.
//
// Specification: §5.4.2. Relying Party Parameters for Credential Generation (https://www.w3.org/TR/webauthn/#dictionary-rp-credential-params)
type RelyingPartyEntity struct {
CredentialEntity
// A unique identifier for the Relying Party entity, which sets the RP ID.
ID string `json:"id"`
}
// The UserEntity represents the PublicKeyCredentialUserEntity IDL and is used to supply additional user account
// attributes when creating a new credential.
//
// Specification: §5.4.3 User Account Parameters for Credential Generation (https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity)
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"`
// 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 any `json:"id"`
}

View File

@@ -0,0 +1,150 @@
package protocol
// Error is a struct that describes specific error conditions in a structured format.
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"`
// Inner error.
Err error `json:"-"`
}
func (e *Error) Error() string {
return e.Details
}
func (e *Error) Unwrap() error {
return e.Err
}
func (e *Error) WithDetails(details string) *Error {
err := *e
err.Details = details
return &err
}
func (e *Error) WithInfo(info string) *Error {
err := *e
err.DevInfo = info
return &err
}
func (e *Error) WithError(err error) *Error {
errCopy := *e
errCopy.Err = err
return &errCopy
}
// ErrorUnknownCredential is a special Error which signals the fact the provided credential is unknown. The reason this
// specific error type is useful is so that the relying-party can send a signal to the Authenticator that the
// credential has been removed.
type ErrorUnknownCredential struct {
Err *Error
}
func (e *ErrorUnknownCredential) Error() string {
return e.Err.Error()
}
func (e *ErrorUnknownCredential) Unwrap() error {
return e.Err
}
func (e *ErrorUnknownCredential) copy() ErrorUnknownCredential {
err := *e.Err
return ErrorUnknownCredential{Err: &err}
}
func (e *ErrorUnknownCredential) WithDetails(details string) *ErrorUnknownCredential {
err := e.copy()
err.Err.Details = details
return &err
}
func (e *ErrorUnknownCredential) WithInfo(info string) *ErrorUnknownCredential {
err := e.copy()
err.Err.DevInfo = info
return &err
}
func (e *ErrorUnknownCredential) WithError(err error) *ErrorUnknownCredential {
errCopy := e.copy()
errCopy.Err.Err = err
return &errCopy
}
var (
ErrBadRequest = &Error{
Type: "invalid_request",
Details: "Error reading the request 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: "attestation_error",
Details: "Error validating the attestation data provided",
}
ErrInvalidAttestation = &Error{
Type: "invalid_attestation",
Details: "Invalid attestation data",
}
ErrMetadata = &Error{
Type: "invalid_metadata",
Details: "",
}
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",
}
)

View File

@@ -0,0 +1,13 @@
package protocol
// Extensions are discussed in §9. WebAuthn Extensions (https://www.w3.org/TR/webauthn/#extensions).
// For a list of commonly supported extensions, see §10. Defined Extensions
// (https://www.w3.org/TR/webauthn/#sctn-defined-extensions).
type AuthenticationExtensionsClientOutputs map[string]any
const (
ExtensionAppID = "appid"
ExtensionAppIDExclude = "appidExclude"
)

View File

@@ -0,0 +1,30 @@
package protocol
import (
"crypto/x509"
)
func init() {
initAndroidKeyHardwareRoots()
initAppleHardwareRoots()
}
func initAndroidKeyHardwareRoots() {
if attAndroidKeyHardwareRootsCertPool == nil {
attAndroidKeyHardwareRootsCertPool = x509.NewCertPool()
}
attAndroidKeyHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAndroidKeyRoot1)))
attAndroidKeyHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAndroidKeyRoot2)))
attAndroidKeyHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAndroidKeyRoot3)))
attAndroidKeyHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAndroidKeyRoot4)))
attAndroidKeyHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAndroidKeyRoot5)))
}
func initAppleHardwareRoots() {
if attAppleHardwareRootsCertPool == nil {
attAppleHardwareRootsCertPool = x509.NewCertPool()
}
attAppleHardwareRootsCertPool.AddCert(mustParseX509CertificatePEM([]byte(certificateAppleRoot1)))
}

View File

@@ -0,0 +1,129 @@
package protocol
import (
"context"
"crypto/x509"
"fmt"
"github.com/google/uuid"
"github.com/go-webauthn/webauthn/metadata"
)
func ValidateMetadata(ctx context.Context, mds metadata.Provider, aaguid uuid.UUID, attestationType, attestationFormat string, x5cs []any) (protoErr *Error) {
if mds == nil {
return nil
}
if AttestationFormat(attestationFormat) == AttestationFormatNone {
return nil
}
var (
entry *metadata.Entry
err error
)
if entry, err = mds.GetEntry(ctx, aaguid); err != nil {
return ErrMetadata.WithInfo(fmt.Sprintf("Failed to validate authenticator metadata for Authenticator Attestation GUID '%s'. Error occurred retreiving the metadata entry: %+v", aaguid, err))
}
if entry == nil {
if aaguid == uuid.Nil && mds.GetValidateEntryPermitZeroAAGUID(ctx) {
return nil
}
if mds.GetValidateEntry(ctx) {
return ErrMetadata.WithInfo(fmt.Sprintf("Failed to validate authenticator metadata for Authenticator Attestation GUID '%s'. The authenticator has no registered metadata.", aaguid))
}
return nil
}
if attestationType != "" && mds.GetValidateAttestationTypes(ctx) {
found := false
for _, atype := range entry.MetadataStatement.AttestationTypes {
if string(atype) == attestationType {
found = true
break
}
}
if !found {
return ErrMetadata.WithInfo(fmt.Sprintf("Failed to validate authenticator metadata for Authenticator Attestation GUID '%s'. The attestation type '%s' is not known to be used by this authenticator.", aaguid.String(), attestationType))
}
}
if mds.GetValidateStatus(ctx) {
if err = mds.ValidateStatusReports(ctx, entry.StatusReports); err != nil {
return ErrMetadata.WithInfo(fmt.Sprintf("Failed to validate authenticator metadata for Authenticator Attestation GUID '%s'. Error occurred validating the authenticator status: %+v", aaguid, err))
}
}
if mds.GetValidateTrustAnchor(ctx) {
if len(x5cs) == 0 {
return nil
}
var (
x5c, parsed *x509.Certificate
x5cis []*x509.Certificate
raw []byte
ok bool
)
for i, x5cAny := range x5cs {
if raw, ok = x5cAny.([]byte); !ok {
return ErrMetadata.WithDetails(fmt.Sprintf("Failed to parse attestation certificate from x5c during attestation validation for Authenticator Attestation GUID '%s'.", aaguid)).WithInfo(fmt.Sprintf("The %s certificate in the attestation was type '%T' but '[]byte' was expected", loopOrdinalNumber(i), x5cAny))
}
if parsed, err = x509.ParseCertificate(raw); err != nil {
return ErrMetadata.WithDetails(fmt.Sprintf("Failed to parse attestation certificate from x5c during attestation validation for Authenticator Attestation GUID '%s'.", aaguid)).WithInfo(fmt.Sprintf("Error returned from x509.ParseCertificate: %+v", err)).WithError(err)
}
if x5c == nil {
x5c = parsed
} else {
x5cis = append(x5cis, parsed)
}
}
if attestationType == string(metadata.AttCA) {
if protoErr = tpmParseAIKAttCA(x5c, x5cis); protoErr != nil {
return ErrMetadata.WithDetails(protoErr.Details).WithInfo(protoErr.DevInfo).WithError(protoErr)
}
}
if x5c != nil && x5c.Subject.CommonName != x5c.Issuer.CommonName {
if !entry.MetadataStatement.AttestationTypes.HasBasicFull() {
return ErrMetadata.WithDetails(fmt.Sprintf("Failed to validate attestation statement signature during attestation validation for Authenticator Attestation GUID '%s'. Attestation was provided in the full format but the authenticator doesn't support the full attestation format.", aaguid))
}
if _, err = x5c.Verify(entry.MetadataStatement.Verifier(x5cis)); err != nil {
return ErrMetadata.WithDetails(fmt.Sprintf("Failed to validate attestation statement signature during attestation validation for Authenticator Attestation GUID '%s'. The attestation certificate could not be verified due to an error validating the trust chain against the Metadata Service.", aaguid)).WithError(err)
}
}
}
return nil
}
func loopOrdinalNumber(n int) string {
n++
if n > 9 && n < 20 {
return fmt.Sprintf("%dth", n)
}
switch n % 10 {
case 1:
return fmt.Sprintf("%dst", n)
case 2:
return fmt.Sprintf("%dnd", n)
case 3:
return fmt.Sprintf("%drd", n)
default:
return fmt.Sprintf("%dth", n)
}
}

View File

@@ -0,0 +1,288 @@
package protocol
import (
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
type CredentialCreation struct {
Response PublicKeyCredentialCreationOptions `json:"publicKey"`
Mediation CredentialMediationRequirement `json:"mediation,omitempty"`
}
type CredentialAssertion struct {
Response PublicKeyCredentialRequestOptions `json:"publicKey"`
Mediation CredentialMediationRequirement `json:"mediation,omitempty"`
}
// PublicKeyCredentialCreationOptions represents the IDL of the same name.
//
// In order to create a Credential via create(), the caller specifies a few parameters in a
// PublicKeyCredentialCreationOptions object.
//
// WebAuthn Level 3: hints,attestationFormats.
//
// Specification: §5.4. Options for Credential Creation (https://www.w3.org/TR/webauthn/#dictionary-makecredentialoptions)
type PublicKeyCredentialCreationOptions struct {
RelyingParty RelyingPartyEntity `json:"rp"`
User UserEntity `json:"user"`
Challenge URLEncodedBase64 `json:"challenge"`
Parameters []CredentialParameter `json:"pubKeyCredParams,omitempty"`
Timeout int `json:"timeout,omitempty"`
CredentialExcludeList []CredentialDescriptor `json:"excludeCredentials,omitempty"`
AuthenticatorSelection AuthenticatorSelection `json:"authenticatorSelection,omitempty"`
Hints []PublicKeyCredentialHints `json:"hints,omitempty"`
Attestation ConveyancePreference `json:"attestation,omitempty"`
AttestationFormats []AttestationFormat `json:"attestationFormats,omitempty"`
Extensions AuthenticationExtensions `json:"extensions,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.
//
// WebAuthn Level 3: hints.
//
// Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dictionary-assertion-options)
type PublicKeyCredentialRequestOptions struct {
Challenge URLEncodedBase64 `json:"challenge"`
Timeout int `json:"timeout,omitempty"`
RelyingPartyID string `json:"rpId,omitempty"`
AllowedCredentials []CredentialDescriptor `json:"allowCredentials,omitempty"`
UserVerification UserVerificationRequirement `json:"userVerification,omitempty"`
Hints []PublicKeyCredentialHints `json:"hints,omitempty"`
Extensions AuthenticationExtensions `json:"extensions,omitempty"`
}
// CredentialDescriptor represents the PublicKeyCredentialDescriptor IDL.
//
// 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.
//
// Specification: §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 URLEncodedBase64 `json:"id"`
// The authenticator transports that can be used.
Transport []AuthenticatorTransport `json:"transports,omitempty"`
// The AttestationType from the Credential. Used internally only.
AttestationType string `json:"-"`
}
func (c CredentialDescriptor) SignalUnknownCredential(rpid string) *SignalUnknownCredential {
return &SignalUnknownCredential{
CredentialID: c.CredentialID,
RPID: rpid,
}
}
// 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"`
}
// CredentialType represents the PublicKeyCredentialType IDL and is used with the CredentialDescriptor IDL.
//
// 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.
//
// Currently one credential type is defined, namely "public-key".
//
// Specification: §5.8.2. Credential Type Enumeration (https://www.w3.org/TR/webauthn/#enumdef-publickeycredentialtype)
//
// Specification: §5.8.3. Credential Descriptor (https://www.w3.org/TR/webauthn/#dictionary-credential-descriptor)
type CredentialType string
const (
// PublicKeyCredentialType - Currently one credential type is defined, namely "public-key".
PublicKeyCredentialType CredentialType = "public-key"
)
// AuthenticationExtensions represents the AuthenticationExtensionsClientInputs IDL. This member contains additional
// parameters requesting additional processing by the client and authenticator.
//
// Specification: §5.7.1. Authentication Extensions Client Inputs (https://www.w3.org/TR/webauthn/#iface-authentication-extensions-client-inputs)
type AuthenticationExtensions map[string]any
// AuthenticatorSelection represents the AuthenticatorSelectionCriteria IDL.
//
// WebAuthn Relying Parties may use the AuthenticatorSelectionCriteria dictionary to specify their requirements
// regarding authenticator attributes.
//
// Specification: §5.4.4. Authenticator Selection Criteria (https://www.w3.org/TR/webauthn/#dictionary-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"`
// ResidentKey this member describes the Relying Party's requirements regarding resident
// credentials per Webauthn Level 2.
ResidentKey ResidentKeyRequirement `json:"residentKey,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"`
}
// ConveyancePreference is the type representing the AttestationConveyancePreference IDL.
//
// WebAuthn Relying Parties may use AttestationConveyancePreference to specify their preference regarding attestation
// conveyance during credential generation.
//
// Specification: §5.4.7. Attestation Conveyance Preference Enumeration (https://www.w3.org/TR/webauthn/#enum-attestation-convey)
type ConveyancePreference string
const (
// PreferNoAttestation is a ConveyancePreference 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 round trip to an Attestation CA or Anonymization CA.
//
// This is the default value.
//
// Specification: §5.4.7. Attestation Conveyance Preference Enumeration (https://www.w3.org/TR/webauthn/#dom-attestationconveyancepreference-none)
PreferNoAttestation ConveyancePreference = "none"
// PreferIndirectAttestation is a ConveyancePreference value.
//
// 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.
//
// Note: There is no guarantee that the Relying Party will obtain a verifiable attestation statement in this case.
// For example, in the case that the authenticator employs self attestation.
//
// Specification: §5.4.7. Attestation Conveyance Preference Enumeration (https://www.w3.org/TR/webauthn/#dom-attestationconveyancepreference-indirect)
PreferIndirectAttestation ConveyancePreference = "indirect"
// PreferDirectAttestation is a ConveyancePreference value.
//
// This value indicates that the Relying Party wants to receive the attestation statement as generated by the
// authenticator.
//
// Specification: §5.4.7. Attestation Conveyance Preference Enumeration (https://www.w3.org/TR/webauthn/#dom-attestationconveyancepreference-direct)
PreferDirectAttestation ConveyancePreference = "direct"
// PreferEnterpriseAttestation is a ConveyancePreference value.
//
// This value indicates that the Relying Party wants to receive an attestation statement that may include uniquely
// identifying information. This is intended for controlled deployments within an enterprise where the organization
// wishes to tie registrations to specific authenticators. User agents MUST NOT provide such an attestation unless
// the user agent or authenticator configuration permits it for the requested RP ID.
//
// If permitted, the user agent SHOULD signal to the authenticator (at invocation time) that enterprise
// attestation is requested, and convey the resulting AAGUID and attestation statement, unaltered, to the Relying
// Party.
//
// Specification: §5.4.7. Attestation Conveyance Preference Enumeration (https://www.w3.org/TR/webauthn/#dom-attestationconveyancepreference-enterprise)
PreferEnterpriseAttestation ConveyancePreference = "enterprise"
)
// AttestationFormat is an internal representation of the relevant inputs for registration.
//
// Specification: §5.4 Options for Credential Creation (https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-attestationformats)
// Registry: https://www.iana.org/assignments/webauthn/webauthn.xhtml
type AttestationFormat string
const (
// AttestationFormatPacked is the "packed" attestation statement format is a WebAuthn-optimized format for
// attestation. It uses a very compact but still extensible encoding method. This format is implementable by
// authenticators with limited resources (e.g., secure elements).
AttestationFormatPacked AttestationFormat = "packed"
// AttestationFormatTPM is the TPM attestation statement format returns an attestation statement in the same format
// as the packed attestation statement format, although the rawData and signature fields are computed differently.
AttestationFormatTPM AttestationFormat = "tpm"
// AttestationFormatAndroidKey is the attestation statement format for platform authenticators on versions "N", and
// later, which may provide this proprietary "hardware attestation" statement.
AttestationFormatAndroidKey AttestationFormat = "android-key"
// AttestationFormatAndroidSafetyNet is the attestation statement format that Android-based platform authenticators
// MAY produce an attestation statement based on the Android SafetyNet API.
AttestationFormatAndroidSafetyNet AttestationFormat = "android-safetynet"
// AttestationFormatFIDOUniversalSecondFactor is the attestation statement format that is used with FIDO U2F
// authenticators.
AttestationFormatFIDOUniversalSecondFactor AttestationFormat = "fido-u2f"
// AttestationFormatApple is the attestation statement format that is used with Apple devices' platform
// authenticators.
AttestationFormatApple AttestationFormat = "apple"
// AttestationFormatCompound is used to pass multiple, self-contained attestation statements in a single ceremony.
AttestationFormatCompound AttestationFormat = "compound"
// AttestationFormatNone is the attestation statement format that is used to replace any authenticator-provided
// attestation statement when a WebAuthn Relying Party indicates it does not wish to receive attestation information.
AttestationFormatNone AttestationFormat = "none"
)
type PublicKeyCredentialHints string
const (
// PublicKeyCredentialHintSecurityKey is a PublicKeyCredentialHint that indicates that the Relying Party believes
// that users will satisfy this request with a physical security key. For example, an enterprise Relying Party may
// set this hint if they have issued security keys to their employees and will only accept those authenticators for
// registration and authentication.
//
// For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the
// authenticatorAttachment SHOULD be set to cross-platform.
PublicKeyCredentialHintSecurityKey PublicKeyCredentialHints = "security-key"
// PublicKeyCredentialHintClientDevice is a PublicKeyCredentialHint that indicates that the Relying Party believes
// that users will satisfy this request with a platform authenticator attached to the client device.
//
// For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the
// authenticatorAttachment SHOULD be set to platform.
PublicKeyCredentialHintClientDevice PublicKeyCredentialHints = "client-device"
// PublicKeyCredentialHintHybrid is a PublicKeyCredentialHint that indicates that the Relying Party believes that
// users will satisfy this request with general-purpose authenticators such as smartphones. For example, a consumer
// Relying Party may believe that only a small fraction of their customers possesses dedicated security keys. This
// option also implies that the local platform authenticator should not be promoted in the UI.
//
// For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the
// authenticatorAttachment SHOULD be set to cross-platform.
PublicKeyCredentialHintHybrid PublicKeyCredentialHints = "hybrid"
)
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 any
type ServerResponse struct {
Status ServerResponseStatus `json:"status"`
Message string `json:"errorMessage"`
}
type ServerResponseStatus string
const (
StatusOk ServerResponseStatus = "ok"
StatusFailed ServerResponseStatus = "failed"
)

View File

@@ -0,0 +1,51 @@
package protocol
// NewSignalAllAcceptedCredentials creates a new SignalAllAcceptedCredentials struct that can simply be encoded with
// json.Marshal.
func NewSignalAllAcceptedCredentials(rpid string, user AllAcceptedCredentialsUser) *SignalAllAcceptedCredentials {
if user == nil {
return nil
}
credentials := user.WebAuthnCredentialIDs()
ids := make([]URLEncodedBase64, len(credentials))
for i, id := range credentials {
ids[i] = id
}
return &SignalAllAcceptedCredentials{
AllAcceptedCredentialIDs: ids,
RPID: rpid,
UserID: user.WebAuthnID(),
}
}
// SignalAllAcceptedCredentials is a struct which represents the CDDL of the same name.
type SignalAllAcceptedCredentials struct {
AllAcceptedCredentialIDs []URLEncodedBase64 `json:"allAcceptedCredentialIds"`
RPID string `json:"rpId"`
UserID URLEncodedBase64 `json:"userId"`
}
// SignalCurrentUserDetails is a struct which represents the CDDL of the same name.
type SignalCurrentUserDetails struct {
DisplayName string `json:"displayName"`
Name string `json:"name"`
RPID string `json:"rpId"`
UserID URLEncodedBase64 `json:"userId"`
}
// SignalUnknownCredential is a struct which represents the CDDL of the same name.
type SignalUnknownCredential struct {
CredentialID URLEncodedBase64 `json:"credentialId"`
RPID string `json:"rpId"`
}
// AllAcceptedCredentialsUser is an interface that can be implemented by a user to provide information about their
// accepted credentials.
type AllAcceptedCredentialsUser interface {
WebAuthnID() []byte
WebAuthnCredentialIDs() [][]byte
}

View File

@@ -0,0 +1,264 @@
package protocol
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
"net/url"
"strings"
"time"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
func mustParseX509Certificate(der []byte) *x509.Certificate {
cert, err := x509.ParseCertificate(der)
if err != nil {
panic(err)
}
return cert
}
func mustParseX509CertificatePEM(raw []byte) *x509.Certificate {
block, rest := pem.Decode(raw)
if len(rest) > 0 || block == nil || block.Type != "CERTIFICATE" {
panic("Invalid PEM Certificate")
}
return mustParseX509Certificate(block.Bytes)
}
func attStatementParseX5CS(attStatement map[string]any, key string) (x5c []any, x5cs []*x509.Certificate, err error) {
var ok bool
if x5c, ok = attStatement[key].([]any); !ok {
return nil, nil, ErrAttestationFormat.WithDetails("Error retrieving x5c value")
}
if len(x5c) == 0 {
return nil, nil, ErrAttestationFormat.WithDetails("Error retrieving x5c value: empty array")
}
if x5cs, err = parseX5C(x5c); err != nil {
return nil, nil, ErrAttestationFormat.WithDetails("Error retrieving x5c value: error occurred parsing values").WithError(err)
}
return x5c, x5cs, nil
}
func parseX5C(x5c []any) (x5cs []*x509.Certificate, err error) {
x5cs = make([]*x509.Certificate, len(x5c))
var (
raw []byte
ok bool
)
for i, t := range x5c {
if raw, ok = t.([]byte); !ok {
return nil, fmt.Errorf("x5c[%d] is not a byte array", i)
}
if x5cs[i], err = x509.ParseCertificate(raw); err != nil {
return nil, fmt.Errorf("x5c[%d] is not a valid certificate: %w", i, err)
}
}
return x5cs, nil
}
// attStatementCertChainVerify allows verifying an attestation statement certificate chain and optionally allows
// mangling the not after value for purpose of just validating the attestation lineage. If you set mangleNotAfter to
// true this function should only be considered safe for determining lineage, and not hte validity of a chain in
// general.
//
// WARNING: Setting mangleNotAfter=true weakens security by accepting expired certificates.
func attStatementCertChainVerify(certs []*x509.Certificate, roots *x509.CertPool, mangleNotAfter bool, mangleNotAfterSafeTime time.Time) (chains [][]*x509.Certificate, err error) {
if len(certs) == 0 {
return nil, errors.New("empty chain")
}
leaf := certs[0]
for _, cert := range certs {
if !cert.IsCA {
leaf = certInsecureConditionalNotAfterMangle(cert, mangleNotAfter, mangleNotAfterSafeTime)
break
}
}
var (
intermediates *x509.CertPool
)
staticRoots := roots != nil
intermediates = x509.NewCertPool()
if roots == nil {
if roots, err = x509.SystemCertPool(); err != nil || roots == nil {
roots = x509.NewCertPool()
}
}
for _, cert := range certs {
if cert == leaf {
continue
}
if isSelfSigned(cert) && !staticRoots {
roots.AddCert(certInsecureConditionalNotAfterMangle(cert, mangleNotAfter, mangleNotAfterSafeTime))
} else {
intermediates.AddCert(certInsecureConditionalNotAfterMangle(cert, mangleNotAfter, mangleNotAfterSafeTime))
}
}
opts := x509.VerifyOptions{
Roots: roots,
Intermediates: intermediates,
}
return leaf.Verify(opts)
}
func isSelfSigned(c *x509.Certificate) bool {
if !c.IsCA {
return false
}
return c.CheckSignatureFrom(c) == nil
}
// This function is used to intentionally but conditionally mangle the certificate not after value to exclude it from
// the verification process. This should only be used in instances where all you care about is which certificates
// performed the signing.
//
// WARNING: Setting mangle=true weakens security by accepting expired certificates.
func certInsecureConditionalNotAfterMangle(cert *x509.Certificate, mangle bool, safe time.Time) (out *x509.Certificate) {
if !mangle || cert.NotAfter.After(safe) {
return cert
}
out = &x509.Certificate{}
*out = *cert
out.NotAfter = safe
return out
}
// This function is used to intentionally mangle the certificate not after value to exclude it from
// the verification process. This should only be used in instances where all you care about is which certificates
// performed the signing.
func certInsecureNotAfterMangle(cert *x509.Certificate, safe time.Time) (out *x509.Certificate) {
c := *cert
out = &c
if out.NotAfter.Before(safe) {
out.NotAfter = safe
}
return out
}
func verifyAttestationECDSAPublicKeyMatch(att AttestationObject, cert *x509.Certificate) (attPublicKeyData webauthncose.EC2PublicKeyData, err error) {
var (
key any
ok bool
publicKey, attPublicKey *ecdsa.PublicKey
)
if key, err = webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey); err != nil {
return attPublicKeyData, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v", err)).WithError(err)
}
if attPublicKeyData, ok = key.(webauthncose.EC2PublicKeyData); !ok {
return attPublicKeyData, ErrInvalidAttestation.WithDetails("Attestation public key is not ECDSA")
}
if publicKey, ok = cert.PublicKey.(*ecdsa.PublicKey); !ok {
return attPublicKeyData, ErrInvalidAttestation.WithDetails("Credential public key is not ECDSA")
}
if attPublicKey, err = attPublicKeyData.ToECDSA(); err != nil {
return attPublicKeyData, ErrInvalidAttestation.WithDetails("Error converting public key to ECDSA").WithError(err)
}
if !attPublicKey.Equal(publicKey) {
return attPublicKeyData, ErrInvalidAttestation.WithDetails("Certificate public key does not match public key in authData")
}
return attPublicKeyData, nil
}
// ValidateRPID performs non-exhaustive checks to ensure the string is most likely a domain string as
// relying-party ID's are required to be. Effectively this can be an IP, localhost, or a string that contains a period.
// The relying-party ID must not contain scheme, port, path, query, or fragment components.
//
// See: https://www.w3.org/TR/webauthn/#rp-id
func ValidateRPID(value string) (err error) {
if len(value) == 0 {
return errors.New("empty value provided")
}
if ip := net.ParseIP(value); ip != nil {
return nil
}
var rpid *url.URL
if rpid, err = url.Parse(value); err != nil {
return err
}
if rpid.Scheme != "" && rpid.Opaque != "" && rpid.Path == "" {
return errors.New("the port component must be empty")
}
if rpid.Scheme != "" {
if rpid.Host != "" && rpid.Path != "" {
return errors.New("the path component must be empty")
}
if rpid.Host != "" && rpid.RawQuery != "" {
return errors.New("the query component must be empty")
}
if rpid.Host != "" && rpid.Fragment != "" {
return errors.New("the fragment component must be empty")
}
if rpid.Host != "" && rpid.Port() != "" {
return errors.New("the port component must be empty")
}
return errors.New("the scheme component must be empty")
}
if rpid.RawQuery != "" {
return errors.New("the query component must be empty")
}
if rpid.RawFragment != "" || rpid.Fragment != "" {
return errors.New("the fragment component must be empty")
}
if rpid.Host == "" {
if strings.Contains(rpid.Path, "/") {
return errors.New("the path component must be empty")
}
}
if value != "localhost" && !strings.Contains(rpid.Path, ".") {
return errors.New("the domain component must actually be a domain")
}
return nil
}

View File

@@ -0,0 +1,33 @@
package webauthncbor
import "github.com/fxamacker/cbor/v2"
const nestedLevelsAllowed = 4
// ctap2CBORDecMode is the cbor.DecMode following the CTAP2 canonical CBOR encoding form
// (https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding)
var ctap2CBORDecMode, _ = cbor.DecOptions{
DupMapKey: cbor.DupMapKeyEnforcedAPF,
MaxNestedLevels: nestedLevelsAllowed,
IndefLength: cbor.IndefLengthForbidden,
TagsMd: cbor.TagsForbidden,
}.DecMode()
var ctap2CBOREncMode, _ = cbor.CTAP2EncOptions().EncMode()
// Unmarshal parses the CBOR-encoded data into the value pointed to by v
// following the CTAP2 canonical CBOR encoding form.
// (https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding)
func Unmarshal(data []byte, v any) error {
// TODO (james-d-elliott): investigate the specific use case for Unmarshal vs UnmarshalFirst to determine the edge cases where this may be useful.
_, err := ctap2CBORDecMode.UnmarshalFirst(data, v)
return err
}
// Marshal encodes the value pointed to by v
// following the CTAP2 canonical CBOR encoding form.
// (https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding)
func Marshal(v any) ([]byte, error) {
return ctap2CBOREncMode.Marshal(v)
}

View File

@@ -0,0 +1,5 @@
package webauthncose
const (
keyCannotDisplay = "Cannot display key"
)

View File

@@ -0,0 +1,10 @@
package webauthncose
import (
"crypto/ed25519"
"crypto/x509"
)
func marshalEd25519PublicKey(pub ed25519.PublicKey) ([]byte, error) {
return x509.MarshalPKIXPublicKey(pub)
}

View File

@@ -0,0 +1,7 @@
package webauthncose
import "math/big"
type ECDSASignature struct {
R, S *big.Int
}

View File

@@ -0,0 +1,13 @@
package webauthncose
import "sync/atomic"
var allowBERIntegers atomic.Bool
// SetExperimentalInsecureAllowBERIntegers allows credentials which have BER integer encoding for their signatures
// which do not conform to the specification. This is an experimental option that may be removed without any notice
// and could potentially lead to zero-day exploits due to the ambiguity of encoding practices. This is not a recommended
// option.
func SetExperimentalInsecureAllowBERIntegers(value bool) {
allowBERIntegers.Store(value)
}

View File

@@ -0,0 +1,589 @@
package webauthncose
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"hash"
"math"
"math/big"
"github.com/go-webauthn/x/encoding/asn1"
"github.com/google/go-tpm/tpm2"
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
)
// 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.
//
// Specification: §6.4.1.1. Examples of credentialPublicKey Values Encoded in COSE_Key Format (https://www.w3.org/TR/webauthn/#sctn-encoded-credPubKey-examples)
type PublicKeyData struct {
// Decode the results to int by default.
_struct bool `cbor:",keyasint" json:"public_key"` //nolint:govet
// 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"`
}
const ecCoordSize = 32
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) {
if err := validateOKPPublicKey(k); err != nil {
return false, err
}
var key ed25519.PublicKey = make([]byte, ed25519.PublicKeySize)
copy(key, k.XCoord)
return ed25519.Verify(key, data, sig), nil
}
// Verify Elliptic Curve Public Key Signature.
func (k *EC2PublicKeyData) Verify(data []byte, sig []byte) (valid bool, err error) {
if err = validateEC2PublicKey(k); err != nil {
return false, err
}
pubkey := &ecdsa.PublicKey{
Curve: ec2AlgCurve(k.Algorithm),
X: big.NewInt(0).SetBytes(k.XCoord),
Y: big.NewInt(0).SetBytes(k.YCoord),
}
h := HasherFromCOSEAlg(COSEAlgorithmIdentifier(k.Algorithm))
h.Write(data)
e := &ECDSASignature{}
var opts []asn1.UnmarshalOpt
if allowBERIntegers.Load() {
opts = append(opts, asn1.WithUnmarshalAllowBERIntegers(true))
}
if _, err = asn1.Unmarshal(sig, e, opts...); err != nil {
return false, ErrSigNotProvidedOrInvalid
}
return ecdsa.Verify(pubkey, h.Sum(nil), e.R, e.S), nil
}
// ToECDSA converts the EC2PublicKeyData to an ecdsa.PublicKey.
func (k *EC2PublicKeyData) ToECDSA() (key *ecdsa.PublicKey, err error) {
if err = validateEC2PublicKey(k); err != nil {
return nil, err
}
return &ecdsa.PublicKey{
Curve: ec2AlgCurve(k.Algorithm),
X: big.NewInt(0).SetBytes(k.XCoord),
Y: big.NewInt(0).SetBytes(k.YCoord),
}, nil
}
// Verify RSA Public Key Signature.
func (k *RSAPublicKeyData) Verify(data []byte, sig []byte) (valid bool, err error) {
if err = validateRSAPublicKey(k); err != nil {
return false, err
}
e, _ := parseRSAPublicKeyDataExponent(k)
pubkey := &rsa.PublicKey{
N: big.NewInt(0).SetBytes(k.Modulus),
E: e,
}
coseAlg := COSEAlgorithmIdentifier(k.Algorithm)
algDetail, ok := COSESignatureAlgorithmDetails[coseAlg]
if !ok {
return false, ErrUnsupportedAlgorithm
}
hash := algDetail.hash
h := hash.New()
h.Write(data)
switch coseAlg {
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
}
}
// ParsePublicKey figures out what kind of COSE material was provided and create the data for the new key.
func ParsePublicKey(keyBytes []byte) (publicKey any, err error) {
pk := PublicKeyData{}
if err = webauthncbor.Unmarshal(keyBytes, &pk); err != nil {
return nil, ErrUnsupportedKey
}
switch COSEKeyType(pk.KeyType) {
case OctetKey:
var o OKPPublicKeyData
if err = webauthncbor.Unmarshal(keyBytes, &o); err != nil {
return nil, err
}
o.PublicKeyData = pk
if err = validateOKPPublicKey(&o); err != nil {
return nil, err
}
return o, nil
case EllipticKey:
var e EC2PublicKeyData
if err = webauthncbor.Unmarshal(keyBytes, &e); err != nil {
return nil, err
}
e.PublicKeyData = pk
if err = validateEC2PublicKey(&e); err != nil {
return nil, err
}
return e, nil
case RSAKey:
var r RSAPublicKeyData
if err = webauthncbor.Unmarshal(keyBytes, &r); err != nil {
return nil, err
}
r.PublicKeyData = pk
if err = validateRSAPublicKey(&r); err != nil {
return nil, err
}
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) (data EC2PublicKeyData, err error) {
x, y := elliptic.Unmarshal(elliptic.P256(), keyBytes)
if x == nil || y == nil {
return data, fmt.Errorf("elliptic unmarshall returned a nil value")
}
return EC2PublicKeyData{
PublicKeyData: PublicKeyData{
KeyType: int64(EllipticKey),
Algorithm: int64(AlgES256),
},
Curve: int64(P256),
XCoord: x.FillBytes(make([]byte, ecCoordSize)),
YCoord: y.FillBytes(make([]byte, ecCoordSize)),
}, nil
}
func VerifySignature(key any, data []byte, sig []byte) (bool, error) {
switch k := key.(type) {
case OKPPublicKeyData:
return k.Verify(data, sig)
case EC2PublicKeyData:
return k.Verify(data, sig)
case RSAPublicKeyData:
return k.Verify(data, sig)
default:
return false, ErrUnsupportedKey
}
}
func DisplayPublicKey(cpk []byte) string {
parsedKey, err := ParsePublicKey(cpk)
if err != nil {
return keyCannotDisplay
}
var data []byte
switch k := parsedKey.(type) {
case RSAPublicKeyData:
var e int
if e, err = parseRSAPublicKeyDataExponent(&k); err != nil {
return keyCannotDisplay
}
rKey := &rsa.PublicKey{
N: big.NewInt(0).SetBytes(k.Modulus),
E: e,
}
if data, err = x509.MarshalPKIXPublicKey(rKey); err != nil {
return keyCannotDisplay
}
case EC2PublicKeyData:
curve := ec2AlgCurve(k.Algorithm)
if curve == nil {
return keyCannotDisplay
}
eKey := &ecdsa.PublicKey{
Curve: curve,
X: big.NewInt(0).SetBytes(k.XCoord),
Y: big.NewInt(0).SetBytes(k.YCoord),
}
if data, err = x509.MarshalPKIXPublicKey(eKey); err != nil {
return keyCannotDisplay
}
case OKPPublicKeyData:
if len(k.XCoord) != ed25519.PublicKeySize {
return keyCannotDisplay
}
var oKey ed25519.PublicKey = make([]byte, ed25519.PublicKeySize)
copy(oKey, k.XCoord)
if data, err = marshalEd25519PublicKey(oKey); err != nil {
return keyCannotDisplay
}
default:
return "Cannot display key of this type"
}
pemBytes := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: data,
})
return string(pemBytes)
}
// COSEAlgorithmIdentifier is 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".
//
// Specification: §5.8.5. Cryptographic Algorithm Identifier (https://www.w3.org/TR/webauthn/#sctn-alg-identifier)
type COSEAlgorithmIdentifier int
const (
// AlgES256 ECDSA with SHA-256.
AlgES256 COSEAlgorithmIdentifier = -7
// AlgEdDSA EdDSA.
AlgEdDSA COSEAlgorithmIdentifier = -8
// AlgES384 ECDSA with SHA-384.
AlgES384 COSEAlgorithmIdentifier = -35
// AlgES512 ECDSA with SHA-512.
AlgES512 COSEAlgorithmIdentifier = -36
// 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
// AlgES256K is ECDSA using secp256k1 curve and SHA-256.
AlgES256K COSEAlgorithmIdentifier = -47
// 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
// AlgRS1 RSASSA-PKCS1-v1_5 with SHA-1.
AlgRS1 COSEAlgorithmIdentifier = -65535
)
// COSEKeyType is The Key type derived from the IANA COSE AuthData.
type COSEKeyType int
const (
// KeyTypeReserved is a reserved value.
KeyTypeReserved COSEKeyType = iota
// OctetKey is an Octet Key.
OctetKey
// EllipticKey is an Elliptic Curve Public Key.
EllipticKey
// RSAKey is an RSA Public Key.
RSAKey
// Symmetric Keys.
Symmetric
// HSSLMS is the public key for HSS/LMS hash-based digital signature.
HSSLMS
)
// COSEEllipticCurve is an enumerator that represents the COSE Elliptic Curves.
//
// Specification: https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves
type COSEEllipticCurve int
const (
// EllipticCurveReserved is the COSE EC Reserved value.
EllipticCurveReserved COSEEllipticCurve = iota
// P256 represents NIST P-256 also known as secp256r1.
P256
// P384 represents NIST P-384 also known as secp384r1.
P384
// P521 represents NIST P-521 also known as secp521r1.
P521
// X25519 for use w/ ECDH only.
X25519
// X448 for use w/ ECDH only.
X448
// Ed25519 for use w/ EdDSA only.
Ed25519
// Ed448 for use w/ EdDSA only.
Ed448
// Secp256k1 is the SECG secp256k1 curve.
Secp256k1
)
func (k *EC2PublicKeyData) TPMCurveID() tpm2.TPMECCCurve {
switch COSEEllipticCurve(k.Curve) {
case P256:
return tpm2.TPMECCNistP256 // TPM_ECC_NIST_P256.
case P384:
return tpm2.TPMECCNistP384 // TPM_ECC_NIST_P384.
case P521:
return tpm2.TPMECCNistP521 // TPM_ECC_NIST_P521.
default:
return tpm2.TPMECCNone // TPM_ECC_NONE.
}
}
func ec2AlgCurve(coseAlg int64) elliptic.Curve {
switch COSEAlgorithmIdentifier(coseAlg) {
case AlgES512: // IANA COSE code for ECDSA w/ SHA-512.
return elliptic.P521()
case AlgES384: // IANA COSE code for ECDSA w/ SHA-384.
return elliptic.P384()
case AlgES256: // IANA COSE code for ECDSA w/ SHA-256.
return elliptic.P256()
default:
return nil
}
}
// SigAlgFromCOSEAlg return which signature algorithm is being used from the COSE Key.
func SigAlgFromCOSEAlg(coseAlg COSEAlgorithmIdentifier) x509.SignatureAlgorithm {
d, ok := COSESignatureAlgorithmDetails[coseAlg]
if !ok {
return x509.UnknownSignatureAlgorithm
}
return d.sigAlg
}
// HasherFromCOSEAlg returns the Hashing interface to be used for a given COSE Algorithm.
func HasherFromCOSEAlg(coseAlg COSEAlgorithmIdentifier) hash.Hash {
d, ok := COSESignatureAlgorithmDetails[coseAlg]
if !ok {
// default to SHA256? Why not.
return crypto.SHA256.New()
}
return d.hash.New()
}
var COSESignatureAlgorithmDetails = map[COSEAlgorithmIdentifier]struct {
name string
hash crypto.Hash
sigAlg x509.SignatureAlgorithm
}{
AlgRS1: {"SHA1-RSA", crypto.SHA1, x509.SHA1WithRSA},
AlgRS256: {"SHA256-RSA", crypto.SHA256, x509.SHA256WithRSA},
AlgRS384: {"SHA384-RSA", crypto.SHA384, x509.SHA384WithRSA},
AlgRS512: {"SHA512-RSA", crypto.SHA512, x509.SHA512WithRSA},
AlgPS256: {"SHA256-RSAPSS", crypto.SHA256, x509.SHA256WithRSAPSS},
AlgPS384: {"SHA384-RSAPSS", crypto.SHA384, x509.SHA384WithRSAPSS},
AlgPS512: {"SHA512-RSAPSS", crypto.SHA512, x509.SHA512WithRSAPSS},
AlgES256: {"ECDSA-SHA256", crypto.SHA256, x509.ECDSAWithSHA256},
AlgES384: {"ECDSA-SHA384", crypto.SHA384, x509.ECDSAWithSHA384},
AlgES512: {"ECDSA-SHA512", crypto.SHA512, x509.ECDSAWithSHA512},
AlgEdDSA: {"EdDSA", crypto.SHA512, x509.PureEd25519},
}
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
}
func validateOKPPublicKey(k *OKPPublicKeyData) error {
if len(k.XCoord) != ed25519.PublicKeySize {
return ErrUnsupportedKey.WithDetails(fmt.Sprintf("OKP key x coordinate has invalid length %d, expected %d", len(k.XCoord), ed25519.PublicKeySize))
}
return nil
}
func validateEC2PublicKey(k *EC2PublicKeyData) error {
curve := ec2AlgCurve(k.Algorithm)
if curve == nil {
return ErrUnsupportedAlgorithm.WithDetails("Unsupported EC2 algorithm")
}
byteLen := (curve.Params().BitSize + 7) / 8
if len(k.XCoord) != byteLen || len(k.YCoord) != byteLen {
return ErrUnsupportedKey.WithDetails("EC2 key x or y coordinate has invalid length")
}
x := new(big.Int).SetBytes(k.XCoord)
y := new(big.Int).SetBytes(k.YCoord)
if !curve.IsOnCurve(x, y) {
return ErrUnsupportedKey.WithDetails("EC2 key point is not on curve")
}
return nil
}
func validateRSAPublicKey(k *RSAPublicKeyData) error {
n := new(big.Int).SetBytes(k.Modulus)
if n.Sign() <= 0 {
return ErrUnsupportedKey.WithDetails("RSA key contains zero or empty modulus")
}
if _, err := parseRSAPublicKeyDataExponent(k); err != nil {
return ErrUnsupportedKey.WithDetails(fmt.Sprintf("RSA key contains invalid exponent: %v", err))
}
return nil
}
func parseRSAPublicKeyDataExponent(k *RSAPublicKeyData) (exp int, err error) {
if k == nil {
return 0, fmt.Errorf("invalid key")
}
if len(k.Exponent) == 0 {
return 0, fmt.Errorf("invalid exponent length")
}
for _, b := range k.Exponent {
if exp > (math.MaxInt >> 8) {
return 0, ErrUnsupportedKey
}
exp = (exp << 8) | int(b)
}
if exp <= 0 {
return 0, ErrUnsupportedKey
}
return exp, nil
}